Mercurial > kallithea
changeset 6967:6c3bda995a88
js: use ajax requests for select2 autocomplete
When you have a big user base, with thousends of users, always using the whole
dataset makes the UI slow.
This will replace kallithea/model/repo.py get_users_js and get_user_groups_js
which were used to inline the full list of users and groups in the document.
Instead, it will expose a json service for doing the completion.
When using the autocomplete, there might be multiple ajax requests, but tests
with a userbase > 9000 showed no problems.
And keep in mind, that although we now make multiple requests (one for every
character) that
- the autocomplete is not used that often
- the requests are quite cheap
- most importanly, we no longer need to calculate the user list/group list if
the user doesn't use the autocomplete
Users and groups are still passed as parameters to the javascript functions -
they will be removed later.
author | domruf <dominikruf@gmail.com> |
---|---|
date | Sun, 06 Aug 2017 12:36:57 +0200 |
parents | 781b28e55b9e |
children | 205daed7185b |
files | kallithea/config/routing.py kallithea/controllers/home.py kallithea/public/js/base.js kallithea/templates/base/root.html kallithea/tests/functional/test_home.py |
diffstat | 5 files changed, 150 insertions(+), 33 deletions(-) [+] |
line wrap: on
line diff
--- a/kallithea/config/routing.py Tue Oct 31 03:17:52 2017 +0100 +++ b/kallithea/config/routing.py Sun Aug 06 12:36:57 2017 +0200 @@ -99,6 +99,8 @@ rmap.connect('about', '/about', controller='home', action='about') rmap.connect('repo_switcher_data', '/_repos', controller='home', action='repo_switcher_data') + rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home', + action='users_and_groups_data') rmap.connect('rst_help', "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
--- a/kallithea/controllers/home.py Tue Oct 31 03:17:52 2017 +0100 +++ b/kallithea/controllers/home.py Sun Aug 06 12:36:57 2017 +0200 @@ -32,13 +32,15 @@ from tg.i18n import ugettext as _ from webob.exc import HTTPBadRequest from sqlalchemy.sql.expression import func +from sqlalchemy import or_, and_ from kallithea.lib.utils import conditional_cache from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator from kallithea.lib.base import BaseController, render, jsonify -from kallithea.model.db import Repository, RepoGroup +from kallithea.lib import helpers as h +from kallithea.model.db import Repository, RepoGroup, User, UserGroup from kallithea.model.repo import RepoModel - +from kallithea.model.scm import UserGroupList log = logging.getLogger(__name__) @@ -142,3 +144,69 @@ 'results': res } return data + + @LoginRequired() + @jsonify + def users_and_groups_data(self): + """ + Returns 'results' with a list of users and user groups. + + You can either use the 'key' GET parameter to get a user by providing + the exact user key or you can use the 'query' parameter to + search for users by user key, first name and last name. + 'types' defaults to just 'users' but can be set to 'users,groups' to + get both users and groups. + No more than 500 results (of each kind) will be returned. + """ + types = request.GET.get('types', 'users').split(',') + key = request.GET.get('key', '') + query = request.GET.get('query', '') + results = [] + if 'users' in types: + user_list = [] + if key: + u = User.get_by_username(key) + if u: + user_list = [u] + elif query: + user_list = User.query() \ + .filter(User.is_default_user == False) \ + .filter(User.active == True) \ + .filter(or_( + User.username.like("%%"+query+"%%"), + User.name.like("%%"+query+"%%"), + User.lastname.like("%%"+query+"%%"), + )) \ + .order_by(User.username) \ + .limit(500) \ + .all() + for u in user_list: + results.append({ + 'type': 'user', + 'id': u.user_id, + 'nname': u.username, + 'fname': u.name, + 'lname': u.lastname, + 'gravatar_lnk': h.gravatar_url(u.email, size=28, default='default'), + 'gravatar_size': 14, + }) + if 'groups' in types: + grp_list = [] + if key: + grp = UserGroup.get_by_group_name(key) + if grp: + grp_list = [grp] + elif query: + grp_list = UserGroup.query() \ + .filter(UserGroup.users_group_name.like("%%"+query+"%%")) \ + .filter(UserGroup.users_group_active == True) \ + .order_by(UserGroup.users_group_name) \ + .limit(500) \ + .all() + for g in UserGroupList(grp_list, perm_level='read'): + results.append({ + 'type': 'group', + 'id': g.users_group_id, + 'grname': g.users_group_name, + }) + return dict(results=results)
--- a/kallithea/public/js/base.js Tue Oct 31 03:17:52 2017 +0100 +++ b/kallithea/public/js/base.js Sun Aug 06 12:36:57 2017 +0200 @@ -1094,7 +1094,7 @@ query = sResultMatch.term.toLowerCase(); // group - if (oResultData.grname) { + if (oResultData.type == "group") { return autocompleteGravatar( "{0}: {1}".format( _TM['Group'], @@ -1118,20 +1118,34 @@ return ''; }; -var SimpleUserAutoComplete = function ($inputElement, users_list) { - $inputElement.select2( - { +var SimpleUserAutoComplete = function ($inputElement) { + $inputElement.select2({ formatInputTooShort: $inputElement.attr('placeholder'), initSelection : function (element, callback) { - var val = $inputElement.val(); - $.each(users_list, function(i, user) { - if (user.nname == val) - callback(user); + $.ajax({ + url: pyroutes.url('users_and_groups_data'), + dataType: 'json', + data: { + key: element.val() + }, + success: function(data){ + callback(data.results[0]); + } }); }, minimumInputLength: 1, - query: function (query) { - query.callback({results: autocompleteMatchUsers(query.term, users_list)}); + ajax: { + url: pyroutes.url('users_and_groups_data'), + dataType: 'json', + data: function(term, page){ + return { + query: term + }; + }, + results: function (data, page){ + return data; + }, + cache: true }, formatSelection: autocompleteFormatter, formatResult: autocompleteFormatter, @@ -1140,31 +1154,32 @@ }); } -var MembersAutoComplete = function ($inputElement, $typeElement, users_list, groups_list) { +var MembersAutoComplete = function ($inputElement, $typeElement) { - var matchAll = function (sQuery) { - var u = autocompleteMatchUsers(sQuery, users_list); - var g = autocompleteMatchGroups(sQuery, groups_list); - return u.concat(g); - }; - - $inputElement.select2( - { + $inputElement.select2({ placeholder: $inputElement.attr('placeholder'), minimumInputLength: 1, - query: function (query) { - query.callback({results: matchAll(query.term)}); + ajax: { + url: pyroutes.url('users_and_groups_data'), + dataType: 'json', + data: function(term, page){ + return { + query: term, + types: 'users,groups' + }; + }, + results: function (data, page){ + return data; + }, + cache: true }, formatSelection: autocompleteFormatter, formatResult: autocompleteFormatter, escapeMarkup: function(m) { return m; }, + id: function(item) { return item.type == 'user' ? item.nname : item.grname }, }).on("select2-selecting", function(e) { // e.choice.id is automatically used as selection value - just set the type of the selection - if (e.choice.nname != undefined) { - $typeElement.val('user'); - } else { - $typeElement.val('users_group'); - } + $typeElement.val(e.choice.type); }); } @@ -1292,13 +1307,23 @@ } /* activate auto completion of users as PR reviewers */ -var PullRequestAutoComplete = function ($inputElement, users_list) { +var PullRequestAutoComplete = function ($inputElement) { $inputElement.select2( { placeholder: $inputElement.attr('placeholder'), minimumInputLength: 1, - query: function (query) { - query.callback({results: autocompleteMatchUsers(query.term, users_list)}); + ajax: { + url: pyroutes.url('users_and_groups_data'), + dataType: 'json', + data: function(term, page){ + return { + query: term + }; + }, + results: function (data, page){ + return data; + }, + cache: true }, formatSelection: autocompleteFormatter, formatResult: autocompleteFormatter, @@ -1312,7 +1337,7 @@ } -function addPermAction(perm_type, users_list, groups_list) { +function addPermAction(perm_type) { var template = '<td><input type="radio" value="{1}.none" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' + '<td><input type="radio" value="{1}.read" checked="checked" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' + @@ -1326,7 +1351,7 @@ var $last_node = $('.last_new_member').last(); // empty tr between last and add var next_id = $('.new_members').length; $last_node.before($('<tr class="new_members">').append(template.format(next_id, perm_type, _TM['Type name of user or member to grant permission']))); - MembersAutoComplete($("#perm_new_member_name_"+next_id), $("#perm_new_member_type_"+next_id), users_list, groups_list); + MembersAutoComplete($("#perm_new_member_name_"+next_id), $("#perm_new_member_type_"+next_id)); } function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
--- a/kallithea/templates/base/root.html Tue Oct 31 03:17:52 2017 +0100 +++ b/kallithea/templates/base/root.html Sun Aug 06 12:36:57 2017 +0200 @@ -106,6 +106,7 @@ pyroutes.register('changeset_home', ${h.js(h.url('changeset_home', repo_name='%(repo_name)s', revision='%(revision)s'))}, ['repo_name', 'revision']); pyroutes.register('repo_size', ${h.js(h.url('repo_size', repo_name='%(repo_name)s'))}, ['repo_name']); pyroutes.register('repo_refs_data', ${h.js(h.url('repo_refs_data', repo_name='%(repo_name)s'))}, ['repo_name']); + pyroutes.register('users_and_groups_data', ${h.js(h.url('users_and_groups_data'))}, []); }); </script>
--- a/kallithea/tests/functional/test_home.py Tue Oct 31 03:17:52 2017 +0100 +++ b/kallithea/tests/functional/test_home.py Sun Aug 06 12:36:57 2017 +0200 @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +import json + from kallithea.tests.base import * from kallithea.tests.fixture import Fixture from kallithea.model.meta import Session @@ -61,3 +64,21 @@ RepoModel().delete(u'gr1/repo_in_group') RepoGroupModel().delete(repo_group=u'gr1', force_delete=True) Session().commit() + + def test_users_and_groups_data(self): + fixture.create_user('evil', firstname=u'D\'o\'ct"o"r', lastname=u'Évíl') + fixture.create_user_group(u'grrrr', user_group_description=u"Groüp") + response = self.app.get(url('users_and_groups_data', query=u'evi')) + result = json.loads(response.body)['results'] + assert result[0].get('fname') == u'D\'o\'ct"o"r' + assert result[0].get('lname') == u'Évíl' + response = self.app.get(url('users_and_groups_data', key=u'evil')) + result = json.loads(response.body)['results'] + assert result[0].get('fname') == u'D\'o\'ct"o"r' + assert result[0].get('lname') == u'Évíl' + response = self.app.get(url('users_and_groups_data', query=u'rrrr')) + result = json.loads(response.body)['results'] + assert not result + response = self.app.get(url('users_and_groups_data', types='users,groups', query=u'rrrr')) + result = json.loads(response.body)['results'] + assert result[0].get('grname') == u'grrrr'