[#5479] Allow user control of automatic tool grouping threshold

Signed-off-by: Cory Johns <johnsca@geek.net>
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 2ba4a56..c0ce5ed 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -203,6 +203,18 @@
 
     @expose()
     @require_post()
+    def configure_tool_grouping(self, grouping_threshold='1', **kw):
+        try:
+            grouping_threshold = int(grouping_threshold)
+            if grouping_threshold < 1:
+                raise ValueError('Invalid threshold')
+            c.project.set_tool_data('allura', grouping_threshold=grouping_threshold)
+        except ValueError as e:
+            flash('Invalid threshold', 'error')
+        redirect('tools')
+
+    @expose()
+    @require_post()
     def update_labels(self, labels=None, **kw):
         require_access(c.project, 'admin')
         c.project.labels = labels.split(',')
diff --git a/Allura/allura/ext/admin/templates/project_tools.html b/Allura/allura/ext/admin/templates/project_tools.html
index 30f72b6..5c62cde 100644
--- a/Allura/allura/ext/admin/templates/project_tools.html
+++ b/Allura/allura/ext/admin/templates/project_tools.html
@@ -115,6 +115,14 @@
 {{c.admin_modal.display(content='<h1 id="popup_title"></h1><div id="popup_contents"></div>')}}
 {{c.mount_delete.display(content='<h1>Confirm Delete</h1>')}}
 <div><!--dummy-->
+
+<h3 style="clear:left">Grouping</h3>
+<form method="POST" action="configure_tool_grouping" id="configure_grouping_form">
+    <label>Threshold for grouping tools by type:
+        <input name="grouping_threshold" value="{{c.project.get_tool_data('allura', 'grouping_threshold', 1)}}"/>
+    </label>
+    <br/><input type="submit" value="Change"/>
+</form>
 {% endblock %}
 
 {% block extra_js %}
@@ -132,5 +140,12 @@
 .pad .fourcol .fleft {
   min-height: 200px;
 }
+
+#configure_grouping_form {
+    padding-left: 10px;
+}
+#configure_grouping_form input[name=grouping_threshold] {
+    width: 1.5em;
+}
 </style>
 {% endblock %}
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index e8263e7..84f346d 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -443,26 +443,25 @@
         grouped_nav = OrderedDict()
         # count how many tools of each type we have
         counts = Counter([e.tool_name.lower() for e in sitemap if e.tool_name])
+        grouping_threshold = self.get_tool_data('allura', 'grouping_threshold', 1)
         for e in sitemap:
             # if it's not a tool, add to navbar and continue
             if not e.tool_name:
                 grouped_nav[id(e)] = e
                 continue
             tool_name = e.tool_name.lower()
-            # tool of a type we don't have in the navbar yet
-            if tool_name not in grouped_nav:
-                # there's more than one tool of this type
-                if counts.get(tool_name, 1) > 1:
+            if counts.get(tool_name, 1) <= grouping_threshold:
+                # don't need grouping, so just add it by label
+                grouped_nav[e.label] = e
+            else:
+                # tool of a type we don't have in the navbar yet
+                if tool_name not in grouped_nav:
                     # change label to be the tool name (type)
                     e.label = tool_name.capitalize()
-                    # add tool url to list of urls that will match this nav entry
-                    e.matching_urls.append(e.url)
                     # change url to point to tool list page
                     e.url = self.url() + '_list/' + tool_name
-                grouped_nav[tool_name] = e
-            else:
-                # already have a tool of this type in the nav; add this tool's
-                # url to the list of urls that match this nav entry
+                    grouped_nav[tool_name] = e
+                # add tool url to list of urls that will match this nav entry
                 grouped_nav[tool_name].matching_urls.append(e.url)
         return grouped_nav.values()
 
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 9c0be46..ad933ab 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -175,6 +175,19 @@
         # that we don't know about
         assert len(set(expected_tools) - set(tool_strings)) == 0, tool_strings
 
+    def test_grouping_threshold(self):
+        r = self.app.get('/admin/tools')
+        grouping_threshold = r.html.find('input',{'name':'grouping_threshold'})
+        assert_equals(grouping_threshold['value'], '1')
+        r = self.app.post('/admin/configure_tool_grouping', params={
+                'grouping_threshold': '2',
+            }).follow()
+        grouping_threshold = r.html.find('input',{'name':'grouping_threshold'})
+        assert_equals(grouping_threshold['value'], '2')
+        r = self.app.get('/admin/tools')
+        grouping_threshold = r.html.find('input',{'name':'grouping_threshold'})
+        assert_equals(grouping_threshold['value'], '2')
+
     def test_project_icon(self):
         file_name = 'neo-icon-set-454545-256x350.png'
         file_path = os.path.join(allura.__path__[0],'nf','allura','images',file_name)
diff --git a/Allura/allura/tests/unit/test_project.py b/Allura/allura/tests/unit/test_project.py
index b59d04f..f33c592 100644
--- a/Allura/allura/tests/unit/test_project.py
+++ b/Allura/allura/tests/unit/test_project.py
@@ -28,3 +28,28 @@
         actual = [(e.label, e.url, len(e.matching_urls))
                 for e in p.grouped_navbar_entries()]
         self.assertEqual(expected, actual)
+
+    def test_grouped_navbar_threshold(self):
+        p = M.Project()
+        sitemap_entries = [
+            SitemapEntry('bugs', url='bugs url', tool_name='Tickets'),
+            SitemapEntry('wiki', url='wiki url', tool_name='Wiki'),
+            SitemapEntry('discuss', url='discuss url', tool_name='Discussion'),
+            SitemapEntry('subproject', url='subproject url'),
+            SitemapEntry('features', url='features url', tool_name='Tickets'),
+            SitemapEntry('help', url='help url', tool_name='Discussion'),
+            SitemapEntry('support reqs', url='support url', tool_name='Tickets'),
+        ]
+        p.url = Mock(return_value='proj_url/')
+        p.sitemap = Mock(return_value=sitemap_entries)
+        p.tool_data['allura'] = {'grouping_threshold': 2}
+        expected = [
+            ('Tickets', 'proj_url/_list/tickets', 3),
+            ('wiki', 'wiki url', 0),
+            ('discuss', 'discuss url', 0),
+            ('subproject', 'subproject url', 0),
+            ('help', 'help url', 0),
+        ]
+        actual = [(e.label, e.url, len(e.matching_urls))
+                for e in p.grouped_navbar_entries()]
+        self.assertEqual(expected, actual)