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'