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