jenkins-bot has submitted this change and it was merged.

Change subject: Add autcomplete to tags on tag insertion
......................................................................


Add autcomplete to tags on tag insertion

Autocomplete will pull from all the tags and will try to complete tags after
the third letter typed. Autocomplete updates when a tag is added.  There
is now a generic bootstrap autocomplete knockout binding that helped
with this feature.

Change-Id: I8ccb3530ff9abce9c53d7a87281674fba193cf81
---
A tests/test_api/test_tag_service.py
M wikimetrics/api/__init__.py
A wikimetrics/api/tags.py
M wikimetrics/controllers/cohorts.py
M wikimetrics/models/storage/__init__.py
M wikimetrics/static/js/cohortList.js
M wikimetrics/static/js/knockout.util.js
M wikimetrics/templates/cohorts.html
8 files changed, 122 insertions(+), 26 deletions(-)

Approvals:
  Nuria: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/tests/test_api/test_tag_service.py 
b/tests/test_api/test_tag_service.py
new file mode 100644
index 0000000..d26d2fd
--- /dev/null
+++ b/tests/test_api/test_tag_service.py
@@ -0,0 +1,27 @@
+from nose.tools import assert_true, assert_equal
+
+from tests.fixtures import DatabaseTest
+from wikimetrics.models import TagStore
+from wikimetrics.api import TagService
+
+
+class TagServiceTest(DatabaseTest):
+    def setUp(self):
+        DatabaseTest.setUp(self)
+        self.tag_service = TagService()
+
+    def test_get_all_tags(self):
+        tag1 = TagStore(name="tag-1")
+        tag2 = TagStore(name="tag-2")
+        self.session.add(tag1)
+        self.session.add(tag2)
+        self.session.commit()
+        tags = self.tag_service.get_all_tags(self.session)
+        assert_true(len(tags), 2)
+        assert_true(tags[0], "tag-1")
+        assert_true(tags[1], "tag-2")
+
+    def test_get_all_tags_empty(self):
+        self.tag_service = TagService()
+        tags = self.tag_service.get_all_tags(self.session)
+        assert_equal(tags, [])
diff --git a/wikimetrics/api/__init__.py b/wikimetrics/api/__init__.py
index 578e234..b3e735e 100644
--- a/wikimetrics/api/__init__.py
+++ b/wikimetrics/api/__init__.py
@@ -1,5 +1,6 @@
 from file_manager import *
 from cohorts import *
+from tags import *
 
 from batch import *
 
diff --git a/wikimetrics/api/tags.py b/wikimetrics/api/tags.py
new file mode 100644
index 0000000..5dda0d3
--- /dev/null
+++ b/wikimetrics/api/tags.py
@@ -0,0 +1,11 @@
+from wikimetrics.models.storage import TagStore
+
+
+class TagService(object):
+    def get_all_tags(self, session):
+        """
+        Gets all the tags by name within the TagStore database
+        """
+        tags = session.query(TagStore.name).order_by(TagStore.name).all()
+        flatten_tags = [item for sublist in tags for item in sublist]
+        return flatten_tags
diff --git a/wikimetrics/controllers/cohorts.py 
b/wikimetrics/controllers/cohorts.py
index 1e8e7c9..30bc13b 100644
--- a/wikimetrics/controllers/cohorts.py
+++ b/wikimetrics/controllers/cohorts.py
@@ -16,7 +16,7 @@
     MediawikiUser, ValidateCohort, TagStore, CohortTagStore
 )
 from wikimetrics.enums import CohortUserRole
-from wikimetrics.api import CohortService
+from wikimetrics.api import CohortService, TagService
 
 
 # TODO: because this is injected by the tests into the REAL controller, it is
@@ -28,8 +28,11 @@
     if request.endpoint is not None:
         if request.path.startswith('/cohorts'):
             cohort_service = getattr(g, 'cohort_service', None)
+            tag_service = getattr(g, 'tag_service', None)
             if cohort_service is None:
                 g.cohort_service = CohortService()
+            if tag_service is None:
+                g.tag_service = TagService()
 
 
 @app.route('/cohorts/')
@@ -38,7 +41,10 @@
     Renders a page with a list cohorts belonging to the currently logged in 
user.
     If the user is an admin, she has the option of seeing other users' cohorts.
     """
-    return render_template('cohorts.html')
+    session = db.get_session()
+    tags = g.tag_service.get_all_tags(session)
+    session.close()
+    return render_template('cohorts.html', tags=json.dumps(tags))
 
 
 @app.route('/cohorts/list/')
@@ -394,6 +400,10 @@
         session.add(cohort_tag)
         session.commit()
         data['tags'] = populate_cohort_tags(cohort_id, session)
+        
+        tagsAutocompleteList = g.tag_service.get_all_tags(session)
+        data['tagsAutocompleteList'] = json.dumps(tagsAutocompleteList)
+        
     except DatabaseError as e:
         session.rollback()
         return json_error(e.message)
@@ -425,6 +435,9 @@
             .filter(CohortTagStore.tag_id == tag_id) \
             .delete()
         session.commit()
-        return json_response(message='success')
+      
+        tags = g.tag_service.get_all_tags(session)
+        return json_response(message='success', 
tagsAutocompleteList=json.dumps(tags))
+        
     finally:
         session.close()
diff --git a/wikimetrics/models/storage/__init__.py 
b/wikimetrics/models/storage/__init__.py
index 47628e1..919c8c4 100644
--- a/wikimetrics/models/storage/__init__.py
+++ b/wikimetrics/models/storage/__init__.py
@@ -1,11 +1,11 @@
+from tag import *
+from cohort_tag import *
 from cohort import *
 from cohort_user import *
 from cohort_wikiuser import *
 from report import *
 from user import *
 from wikiuser import *
-from tag import *
-from cohort_tag import *
 
 # ignore flake8 because of F403 violation
 # flake8: noqa
diff --git a/wikimetrics/static/js/cohortList.js 
b/wikimetrics/static/js/cohortList.js
index 72edbc7..9bc3434 100644
--- a/wikimetrics/static/js/cohortList.js
+++ b/wikimetrics/static/js/cohortList.js
@@ -1,9 +1,19 @@
+/*global $:false */
+/*global ko:false */
+/*global document*/
+/*global site*/
+/*global setTimeout*/
 $(document).ready(function(){
-    
+    var initialTagList = [];
+    try {
+        initialTagList = JSON.parse($('#tagsForAutocomplete').text());
+    } catch (e){}
+
     var viewModel = {
         filter: ko.observable(''),
         cohorts: ko.observableArray([]),
-        
+        tagsAutocompleteList: ko.observableArray(initialTagList),
+
         populate: function(cohort, data){
             cohort.validated(data.validated);
             cohort.wikiusers(data.wikiusers);
@@ -18,14 +28,18 @@
             cohort.total_count(v.total_count);
             cohort.validation_status(v.validation_status);
         },
-        
+
         _populateTags: function(cohort, data){
             cohort.tags(data.tags.map(function(t){
                 t.highlight = ko.observable(false);
                 return t;
             }));
         },
-        
+
+        _populateAutocomplete: function(data){
+            this.tagsAutocompleteList(JSON.parse(data.tagsAutocompleteList));
+        },
+
         view: function(cohort, event, callback){
             $.get('/cohorts/detail/' + cohort.id)
                 .done(site.handleWith(function(data){
@@ -36,7 +50,7 @@
                 }))
                 .fail(site.failure);
         },
-        
+
         loadWikiusers: function(cohort, event){
             $.get('/cohorts/detail/' + cohort.id + '?full_detail=true')
                 .done(site.handleWith(function(data){
@@ -45,7 +59,7 @@
                 }))
                 .fail(site.failure);
         },
-        
+
         deleteCohort: function(cohort, event){
             if (site.confirmDanger(event, true)){
                 $.post('/cohorts/delete/' + cohort.id)
@@ -55,7 +69,7 @@
                     .fail(site.failure);
             }
         },
-        
+
         validateWikiusers: function(cohort, event){
             if (site.confirmDanger(event)){
                 $.post('/cohorts/validate/' + cohort.id)
@@ -67,17 +81,16 @@
                     .fail(site.failure);
             }
         },
-        
-        addTag: function(form){
+
+        addTag: function(){
             /*
              * turns tag lowercase and replaces ' ' with '-'
              */
             function parseTag(tag){
                 return tag.replace(/\s+/g, "-").toLowerCase();
             }
-            
             var cohort = this;
-            var tag = parseTag(cohort.tag_name());
+            var tag = parseTag(cohort.tag_name_to_add());
             //Make sure match is exact
             var existing = null;
             $.each(cohort.tags(), function(){
@@ -94,6 +107,7 @@
                         }
                         else{
                             viewModel._populateTags(cohort, data);
+                            viewModel._populateAutocomplete(data);
                         }
                     }))
                     .fail(site.failure);
@@ -103,18 +117,20 @@
                     existing.highlight(false);
                 }, 1500);
             }
-            cohort.tag_name('');
+            cohort.tag_name_to_add('');
         },
-        
+
         deleteTag: function(event, cohort, tag){
             $.post('/cohorts/' +  cohort.id + '/tag/delete/' + tag.id)
                 .done(site.handleWith(function(data){
                     cohort.tags.remove(tag);
+                    // NOTE: autocomplete doesn't change
+                    // because tags are only removed from the cohort
                 }))
                 .fail(site.failure);
         }
     };
-    
+
     viewModel.filteredCohorts = ko.computed(function(){
         if (this.cohorts().length && this.filter().length) {
             var filter = this.filter().toLowerCase();
@@ -125,7 +141,7 @@
         }
         return this.cohorts();
     }, viewModel);
-    
+
     // fetch this user's cohorts
     $.get('/cohorts/list/?include_invalid=true')
         .done(site.handleWith(function(data){
@@ -134,11 +150,12 @@
             site.enableTabNavigation();
         }))
         .fail(site.failure);
-    
+
     ko.applyBindings(viewModel);
-    
+
     function setBlankProperties(list){
-        bareList = ko.utils.unwrapObservable(list);
+
+        var bareList = ko.utils.unwrapObservable(list);
         ko.utils.arrayForEach(bareList, function(item){
             // TODO: auto-map the new properties
             item.wikiusers = ko.observableArray([]);
@@ -150,7 +167,7 @@
             item.total_count = ko.observable(0);
             item.validation_status = ko.observable();
             item.delete_message = ko.observable();
-            item.tag_name = ko.observable();
+            item.tag_name_to_add = ko.observable();
             item.tags = ko.observableArray([]);
 
             item.can_run_report = ko.computed(function(){
diff --git a/wikimetrics/static/js/knockout.util.js 
b/wikimetrics/static/js/knockout.util.js
index 1285202..640cccb 100644
--- a/wikimetrics/static/js/knockout.util.js
+++ b/wikimetrics/static/js/knockout.util.js
@@ -34,3 +34,27 @@
         }
     }
 };
+
+/**
+ * Custom binding that adds bootstrap typeahead functionality to any input:
+ * `<input data-bind="autocomplete: property()"></section>`
+ * And works as follows:
+ *     In the example above, property is a ko.observableArray holding an 
autocomplete list
+ */
+ko.bindingHandlers.autocomplete = {
+    init: function(element){
+        $(element).attr('autocomplete', 'off');
+        $(element).data('provide', 'typeahead');
+    },
+    update: function(element, valueAccessor) {
+        var unwrapped = ko.unwrap(valueAccessor);
+
+        if (unwrapped !== null) {
+            // typeaheads are made to be unmutable in bootstrap
+            // so we 'kind of' destroy it and create it again
+            $(element).data('typeahead', null);
+            $(element).unbind('keyup');
+            $(element).typeahead({'source': unwrapped, 'minLength': 2});
+        }
+    }
+};
diff --git a/wikimetrics/templates/cohorts.html 
b/wikimetrics/templates/cohorts.html
index 61f6b9d..0c85213 100644
--- a/wikimetrics/templates/cohorts.html
+++ b/wikimetrics/templates/cohorts.html
@@ -2,12 +2,13 @@
 {% block body %}
 
 <div class="page-header">
-    <h2>Cohorts 
+    <h2>Cohorts
         <input type="text" class="search-query" placeholder="type to filter 
your search" data-bind="value: filter, valueUpdate:'afterkeydown'"/>
         <small class="pull-right">
             <a class="btn btn-primary " 
href="{{url_for('cohort_upload')}}">Upload a New Cohort</a>
         </small>
     </h2>
+    <span id="tagsForAutocomplete" style="display:none">{{tags}}</span>
 </div>
 <div class="tabbable tabs-left">
       <ul class="nav nav-tabs" data-bind="foreach: filteredCohorts">
@@ -29,7 +30,9 @@
                 </div>
                 <div>
                         <form class="navbar-form pull-left" data-bind="submit: 
$root.addTag.bind($data)">
-                            <input data-bind= "value: tag_name" type="text" 
class="span4" required>
+                            <input type="text"
+                                   data-bind="value: tag_name_to_add, 
autocomplete: $root.tagsAutocompleteList()"
+                                    />
                             <input type="submit" class="btn small" value="Add 
Tag"/>
                         </form>
                 </div>

-- 
To view, visit https://gerrit.wikimedia.org/r/145039
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I8ccb3530ff9abce9c53d7a87281674fba193cf81
Gerrit-PatchSet: 12
Gerrit-Project: analytics/wikimetrics
Gerrit-Branch: master
Gerrit-Owner: Terrrydactyl <[email protected]>
Gerrit-Reviewer: Milimetric <[email protected]>
Gerrit-Reviewer: Nuria <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to