You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2016/11/08 19:26:23 UTC

[4/4] allura git commit: [#8135] Make category selection awesome

[#8135] Make category selection awesome

* Omit unnecessary top-level category name
* Nicer separator characters
* Make drop-downs searchable
* Configurable order of categories
* Configurable help text for each
* Configurable recommended choices for each


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/07b45f37
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/07b45f37
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/07b45f37

Branch: refs/heads/db/8135
Commit: 07b45f37cd4794002d7ac5f9941af3d4290bc6eb
Parents: 4d28325
Author: Dave Brondsema <da...@brondsema.net>
Authored: Wed Oct 12 12:47:50 2016 -0400
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Tue Nov 8 14:26:06 2016 -0500

----------------------------------------------------------------------
 Allura/allura/controllers/project.py            |   1 -
 Allura/allura/ext/admin/admin_main.py           |  30 ++++--
 .../ext/admin/templates/project_trove.html      | 107 +++++++++++++++----
 Allura/allura/model/project.py                  |  12 ++-
 Allura/allura/tests/functional/test_admin.py    |   7 +-
 .../tests/functional/test_neighborhood.py       |   5 +-
 Allura/development.ini                          |   4 +
 7 files changed, 129 insertions(+), 37 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 5e9a510..5f956d9 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -60,7 +60,6 @@ class W:
     add_project = plugin.ProjectRegistrationProvider.get().add_project_widget(antispam=True)
     page_list = ffw.PageList()
     page_size = ffw.PageSize()
-    project_select = ffw.NeighborhoodProjectSelect
     neighborhood_overview_form = ff.NeighborhoodOverviewForm()
     award_grant_form = ff.AwardGrantForm
 

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 3759098..1bdc58b 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -16,10 +16,11 @@
 #       under the License.
 
 import logging
+from collections import OrderedDict
 from datetime import datetime
 from urlparse import urlparse
 import json
-from operator import itemgetter
+from operator import itemgetter, attrgetter
 import pkg_resources
 from pylons import tmpl_context as c, app_globals as g, response
 from pylons import request
@@ -253,13 +254,24 @@ class ProjectAdminController(BaseController):
     @expose('jinja:allura.ext.admin:templates/project_trove.html')
     def trove(self):
         c.label_edit = W.label_edit
-        base_troves = M.TroveCategory.query.find(
-            dict(trove_parent_id=0)).sort('fullname').all()
-        topic_trove = M.TroveCategory.query.get(
-            trove_parent_id=0, shortname='topic')
-        license_trove = M.TroveCategory.query.get(
-            trove_parent_id=0, shortname='license')
-        return dict(base_troves=base_troves, license_trove=license_trove, topic_trove=topic_trove)
+        base_troves_by_name = {t.shortname: t
+                               for t in M.TroveCategory.query.find(dict(trove_parent_id=0))}
+        first_troves = aslist(config.get('trovecategories.admin.order', 'topic,license,os'), ',')
+        base_troves = [
+            base_troves_by_name.pop(t) for t in first_troves
+        ] + sorted(base_troves_by_name.values(), key=attrgetter('fullname'))
+
+        trove_recommendations = {}
+        for trove in base_troves:
+            config_name = 'trovecategories.admin.recommended.{}'.format(trove.shortname)
+            recommendation_pairs = aslist(config.get(config_name, []), ',')
+            trove_recommendations[trove.shortname] = OrderedDict()
+            for pair in recommendation_pairs:
+                trove_id, label = pair.split('=')
+                trove_recommendations[trove.shortname][trove_id] = label
+
+        return dict(base_troves=base_troves,
+                    trove_recommendations=trove_recommendations)
 
     @expose('jinja:allura.ext.admin:templates/project_tools_moved.html')
     def tools_moved(self, **kw):
@@ -472,7 +484,7 @@ class ProjectAdminController(BaseController):
     def add_trove_js(self, type, new_trove, **kw):
         require_access(c.project, 'update')
         trove_obj, error_msg = self._add_trove(type, new_trove)
-        return dict(trove_full_path=trove_obj.fullpath, trove_cat_id=trove_obj.trove_cat_id, error_msg=error_msg)
+        return dict(trove_full_path=trove_obj.fullpath_within_type, trove_cat_id=trove_obj.trove_cat_id, error_msg=error_msg)
 
     @expose()
     @require_post()

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/ext/admin/templates/project_trove.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/project_trove.html b/Allura/allura/ext/admin/templates/project_trove.html
index dd7bd51..81173cf 100644
--- a/Allura/allura/ext/admin/templates/project_trove.html
+++ b/Allura/allura/ext/admin/templates/project_trove.html
@@ -24,10 +24,17 @@
 
 {% macro show_trove_base_cat(base) %}
   <h3>{{base.fullname}}</h3>
+  {% set help_text = config.get('trovecategories.admin.help.'+base.shortname, '') %}
+  {% if help_text %}
+    <div class="grid-19">
+      {{ help_text|safe }}
+      <br><br>
+    </div>
+  {% endif %}
   <div id="trove_existing_{{base.shortname}}" class="trove_existing grid-19">
     {% for cat in c.project.troves_by_type(base.shortname)|sort(attribute='fullpath') %}
     <div style="clear: both">
-      <span class="trove_fullpath">{{cat.fullpath}}</span>
+      <span class="trove_fullpath">{{cat.fullpath_within_type}}</span>
       <form id="delete_trove_{{base.shortname}}_{{cat.trove_cat_id}}"
             action="delete_trove" method="post" class="trove_deleter">
         <input type="hidden" name="type" value="{{base.shortname}}">
@@ -41,14 +48,27 @@
   {% endfor %}
   </div>
   <div class="grid-19 trove_add_container">
+    {% if trove_recommendations[base.shortname] %}
+        Recommended to choose from:
+            {% for trove_id, label in trove_recommendations[base.shortname].iteritems() %}
+                <a href="#" data-id="{{ trove_id }}" data-trove="{{ base.shortname }}" class="recommended_trove"><i class="fa fa-plus-circle"></i> {{ label }}</a>
+            {% endfor %}
+        <br>
+        Or <a href="#" class="choose_other">choose from other options...</a>
+    {% else %}
+        <label for="new_trove_{{base.shortname}}">Add a new {{base.fullname}} category:</label>
+        <br>
+    {% endif %}
+
     <form id="add_trove_{{base.shortname}}"
-          action="add_trove" method="post" class="trove_adder">
+          action="add_trove" method="post" class="trove_adder"
+          {% if trove_recommendations[base.shortname] %}style="display:none"{% endif -%}
+          >
       <input type="hidden" name="type" value="{{base.shortname}}">
-      <label for="new_trove_{{base.shortname}}">Add a new {{base.fullname}} category:</label>
-      <br>
-      <select name="new_trove" id="new_trove_{{base.shortname}}">
+      <select name="new_trove" id="new_trove_{{base.shortname}}" data-placeholder="Choose one...">
+        <option value=""></option>
         {% for cat in base.children if not cat.parent_only %}
-          <option value="{{cat.trove_cat_id}}">{{cat.fullpath}}</option>
+            <option value="{{cat.trove_cat_id}}">{{cat.fullpath_within_type}}</option>
         {% endfor %}
       </select>
       <br>
@@ -63,6 +83,7 @@
     <div class="notice">This project has been deleted and is not visible to non-admin users</div>
   {% endif %}
 
+  <div class="project_labels">
     <h3>Project Labels</h3>
     <div class="grid-19 trove_add_container">
       <form method="POST" class="can-retry" action="update_labels" id="label_edit_form">
@@ -72,23 +93,22 @@
         {{lib.csrf_token()}}
       </form>
     </div>
-  {{show_trove_base_cat(topic_trove)}}
-  {{show_trove_base_cat(license_trove)}}
-  {% for base in base_troves if base.shortname != 'topic' and base.shortname != 'license' %}
+  </div>
+  {% for base in base_troves %}
     {{show_trove_base_cat(base)}}
   {% endfor %}
 {% endblock %}
 
+{% do g.register_forge_js('js/chosen.jquery.min.js') %}
+{% do g.register_forge_css('css/chosen.min.css') %}
+
 {% block extra_js %}
   <script type="text/javascript">
     $(document).ready(function () {
-      var session_id = $('input[name=_session_id]').val();
-      var del_btn = '<a href="#" class="del_btn" title="Delete"><b data-icon="{{g.icons["delete"].char}}" class="ico {{g.icons["delete"].css}}"></b></a>';
-      $('form.trove_adder').submit(function(evt){
-        evt.preventDefault();
-        var $this = $(this);
-        var type = $this.find('input[name=type]').val();
-        var new_id = $this.find('select').last().val();
+      var chosen_opts = {search_contains:true};
+      $('.trove_add_container form:visible select').chosen(chosen_opts);
+
+      function add_trove(session_id, type, new_id) {
         $.post('add_trove_js',{
           _session_id:session_id,
           type:type,
@@ -116,6 +136,16 @@
               }
             }
         });
+      }
+
+      var session_id = $('input[name=_session_id]').val();
+      var del_btn = '<a href="#" class="del_btn" title="Delete"><b data-icon="{{g.icons["delete"].char}}" class="ico {{g.icons["delete"].css}}"></b></a>';
+      $('form.trove_adder').submit(function(evt){
+        evt.preventDefault();
+        var $this = $(this);
+        var type = $this.find('input[name=type]').val();
+        var new_id = $this.find('select').last().val();
+        add_trove(session_id, type, new_id);
       });
       $('form.trove_deleter').each(function(){
         $(this).find('input[type="submit"]').remove();
@@ -136,15 +166,24 @@
             }
         });
       });
+      $('a.choose_other').on('click', function(e){
+        e.preventDefault();
+        var $form = $(this).next('form');
+        $form.show();
+        $('select', $form).chosen(chosen_opts);
+      });
+      $('a.recommended_trove').on('click', function(e) {
+        e.preventDefault();
+        var type = $(this).data('trove');
+        var new_id = $(this).data('id');
+        add_trove(session_id, type, new_id);
+      })
     });
   </script>
 {% endblock %}
 
 {% block extra_css %}
   <style type="text/css">
-    .trove_adder select {
-        font: 90% sans-serif;
-    }
     .trove_deleter{
       display:inline;
     }
@@ -157,11 +196,39 @@
     .trove_existing{
       margin-bottom: 1em;
     }
-    .trove_add_container{
+    .trove_add_container {
       margin-bottom: 1em;
       padding-bottom: 1em;
       border: 0 solid #ccc;
       border-width: 0 0 1px 0;
     }
+    .recommended_trove {
+      margin-right: 0.5em;
+    }
+
+    /* for Chosen plugin to display well.  Super-long thread with other possible options: https://github.com/harvesthq/chosen/issues/86 */
+    .trove_add_container{
+      overflow: visible;
+    }
+    :not(.project_labels) > .trove_add_container:last-of-type {
+      margin-bottom: 250px;
+    }
+    /* for Chosen plugin, use Font-Awesome for icons instead of their sprite
+      Because when we have debug=False in the .ini file, the CSS concatenation makes the path to the sprite incorrect */
+    .chosen-container-single .chosen-search:after {
+      font-family: FontAwesome;
+      content: "\f002";  /* fa-search */
+      position: relative;
+      left: -1.5em;
+    }
+    .chosen-container-single .chosen-single div b {
+      background: none; /* cancel out existing sprite, so its not duplicated */
+    }
+    .chosen-container-single .chosen-single div b:after {
+      content: "\u25be";
+    }
+    .chosen-container-single.chosen-with-drop .chosen-single div b:after {
+      content: "\u25b4";
+    }
   </style>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 963145e..3b36ea2 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -1,3 +1,4 @@
+# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -131,11 +132,13 @@ class TroveCategory(MappedClass):
 
     @property
     def subcategories(self):
-        return self.query.find(dict(trove_parent_id=self.trove_cat_id)).sort('fullname').all()
+        return sorted(self.query.find(dict(trove_parent_id=self.trove_cat_id)).all(),
+                      key=lambda t: t.fullname.lower())
 
     @property
     def children(self):
-        return self.query.find({'fullpath': re.compile('^' + re.escape(self.fullpath) + ' ::')}).sort('fullpath')
+        return sorted(self.query.find({'fullpath': re.compile('^' + re.escape(self.fullpath) + ' ::')}).all(),
+                      key=lambda t: t.fullpath.lower())
 
     @property
     def type(self):
@@ -162,6 +165,11 @@ class TroveCategory(MappedClass):
             crumbs.append((trove.fullname, url))
         return crumbs
 
+    @property
+    def fullpath_within_type(self):
+        'remove first section of full path, and use nicer separator'
+        return u' � '.join(self.fullpath.split(' :: ')[1:])
+
     def __json__(self):
         return dict(
             id=self.trove_cat_id,

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/tests/functional/test_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index f5f49e9..98dda1b 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -1,3 +1,4 @@
+# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -548,7 +549,7 @@ class TestProjectAdmin(TestController):
 
         r = self.app.get('/admin/trove')
         assert 'No Database Environment categories have been selected.' in r
-        assert '<span class="trove_fullpath">Database Environment :: Database API</span>' not in r
+        assert '<span class="trove_fullpath">Database API</span>' not in r
         # add a cat
         with audits('add trove root_database: Database Environment :: Database API'):
             form = r.forms['add_trove_root_database']
@@ -556,13 +557,13 @@ class TestProjectAdmin(TestController):
             r = form.submit().follow()
         # make sure it worked
         assert 'No Database Environment categories have been selected.' not in r
-        assert '<span class="trove_fullpath">Database Environment :: Database API :: Python Database API</span>' in r
+        assert u'<span class="trove_fullpath">Database API � Python Database API</span>' in r
         # delete the cat
         with audits('remove trove root_database: Database Environment :: Database API'):
             r = r.forms['delete_trove_root_database_506'].submit().follow()
         # make sure it worked
         assert 'No Database Environment categories have been selected.' in r
-        assert '<span class="trove_fullpath">Database Environment :: Database API :: Python Database API</span>' not in r
+        assert u'<span class="trove_fullpath">Database API � Python Database API</span>' not in r
 
     def test_add_remove_label(self):
         setup_trove_categories()

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/allura/tests/functional/test_neighborhood.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index fb48faa..e11dbac 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -1,3 +1,4 @@
+# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -741,8 +742,8 @@ class TestNeighborhood(TestController):
         # check the labels and trove cats
         r = self.app.get('/adobe/testtemp/admin/trove')
         assert 'mmi' in r
-        assert 'Topic :: Communications :: Telephony' in r
-        assert 'Development Status :: 5 - Production/Stable' in r
+        assert u'Communications � Telephony' in r
+        assert '5 - Production/Stable' in r
         # check the wiki text
         r = self.app.get('/adobe/testtemp/wiki/').follow()
         assert "My home text!" in r

http://git-wip-us.apache.org/repos/asf/allura/blob/07b45f37/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 6e9c1d3..b5d6072 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -427,6 +427,10 @@ trovecategories.enableediting = true
 ; Site admins only:
 ;trovecategories.enableediting = admin
 
+trovecategories.admin.recommended.license = 188=MIT,401=Apache,679=GPL,680=LGPL,670=AGPL
+trovecategories.admin.recommended.os = 65=Windows,309=Mac OSX,201=Linux,728=Android,780=iOS
+trovecategories.admin.help.license = For help choosing a license, visit <a href="http://choosealicense.com/">http://choosealicense.com/</a>
+
 ; ActivityStream
 activitystream.master = mongodb://127.0.0.1:27017
 activitystream.database = activitystream