Pārlūkot izejas kodu

HUE-7774 [core] Increase the limit of User and Group list in Hue Autocomplete box

Roohi 8 gadi atpakaļ
vecāks
revīzija
f6937f7d3f

+ 32 - 18
apps/useradmin/src/useradmin/tests.py

@@ -711,49 +711,63 @@ class TestUserAdmin(BaseUserAdminTests):
   def test_list_for_autocomplete(self):
 
     # Now the autocomplete has access to all the users and groups
-    c1 = make_logged_in_client('test_list_for_autocomplete', is_superuser=False, groupname='test_list_for_autocomplete')
-    c2_same_group = make_logged_in_client('test_list_for_autocomplete2', is_superuser=False, groupname='test_list_for_autocomplete')
-    c3_other_group = make_logged_in_client('test_list_for_autocomplete3', is_superuser=False, groupname='test_list_for_autocomplete_other_group')
+    c1 = make_logged_in_client('user_test_list_for_autocomplete', is_superuser=False, groupname='group_test_list_for_autocomplete')
+    c2_same_group = make_logged_in_client('user_test_list_for_autocomplete2', is_superuser=False, groupname='group_test_list_for_autocomplete')
+    c3_other_group = make_logged_in_client('user_test_list_for_autocomplete3', is_superuser=False, groupname='group_test_list_for_autocomplete_other_group')
 
-    # c1 users should list only 'test_list_for_autocomplete2' and group should not list 'test_list_for_autocomplete_other_group'
-    response = c1.get(reverse('useradmin.views.list_for_autocomplete'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+    # c1 users should list only 'user_test_list_for_autocomplete2' and group should not list 'group_test_list_for_autocomplete_other_group'
+    response = c1.get(reverse('useradmin.views.list_for_autocomplete'))
     content = json.loads(response.content)
 
     users = [smart_unicode(user['username']) for user in content['users']]
     groups = [smart_unicode(user['name']) for user in content['groups']]
 
-    assert_equal([u'test_list_for_autocomplete2'], users)
-    assert_true(u'test_list_for_autocomplete' in groups, groups)
-    assert_false(u'test_list_for_autocomplete_other_group' in groups, groups)
+    assert_equal([u'user_test_list_for_autocomplete2'], users)
+    assert_true(u'group_test_list_for_autocomplete' in groups, groups)
+    assert_false(u'group_test_list_for_autocomplete_other_group' in groups, groups)
 
     # only_mygroups has no effect if user is not super user
-    response = c1.get(reverse('useradmin.views.list_for_autocomplete'), {'only_mygroups':False, 'include_myself':True}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response = c1.get(reverse('useradmin.views.list_for_autocomplete'), {'include_myself': True})
     content = json.loads(response.content)
 
     users = [smart_unicode(user['username']) for user in content['users']]
     groups = [smart_unicode(user['name']) for user in content['groups']]
 
-    assert_equal([u'test_list_for_autocomplete',u'test_list_for_autocomplete2'], users)
-    assert_true(u'test_list_for_autocomplete' in groups, groups)
-    assert_false(u'test_list_for_autocomplete_other_group' in groups, groups)
+    assert_equal([u'user_test_list_for_autocomplete', u'user_test_list_for_autocomplete2'], users)
+    assert_true(u'group_test_list_for_autocomplete' in groups, groups)
+    assert_false(u'group_test_list_for_autocomplete_other_group' in groups, groups)
 
     # c3 is alone
-    response = c3_other_group.get(reverse('useradmin.views.list_for_autocomplete'), {'include_myself':True}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response = c3_other_group.get(reverse('useradmin.views.list_for_autocomplete'), {'include_myself': True})
     content = json.loads(response.content)
 
     users = [smart_unicode(user['username']) for user in content['users']]
     groups = [smart_unicode(user['name']) for user in content['groups']]
 
-    assert_equal([u'test_list_for_autocomplete3'], users)
-    assert_true(u'test_list_for_autocomplete_other_group' in groups, groups)
+    assert_equal([u'user_test_list_for_autocomplete3'], users)
+    assert_true(u'group_test_list_for_autocomplete_other_group' in groups, groups)
 
-    # superuser should get all users
     c4_super_user = make_logged_in_client(is_superuser=True)
-    response = c4_super_user.get('/desktop/api/users/autocomplete', {'include_myself':True,'only_mygroups':True}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+    # superuser should get all users as autocomplete filter is not passed
+    response = c4_super_user.get('/desktop/api/users/autocomplete', {'include_myself': True, 'only_mygroups': True})
     content = json.loads(response.content)
 
     users = [smart_unicode(user['username']) for user in content['users']]
-    assert_equal([u'test', u'test_list_for_autocomplete', u'test_list_for_autocomplete2', u'test_list_for_autocomplete3'],  users)
+    assert_equal([u'test', u'user_test_list_for_autocomplete', u'user_test_list_for_autocomplete2', u'user_test_list_for_autocomplete3'], users)
+
+    c5_autocomplete_filter_by_groupname = make_logged_in_client('user_doesnt_match_autocomplete_filter',is_superuser=False,groupname='group_test_list_for_autocomplete')
+
+    # superuser should get all users & groups which match the autocomplete filter case insensitive
+    response = c4_super_user.get('/desktop/api/users/autocomplete', {'include_myself': True, 'filter': 'Test_list_for_autocomplete'})
+    content = json.loads(response.content)
+
+    users = [smart_unicode(user['username']) for user in content['users']]
+    groups = [smart_unicode(user['name']) for user in content['groups']]
+
+    assert_equal([ u'user_test_list_for_autocomplete', u'user_test_list_for_autocomplete2', u'user_test_list_for_autocomplete3'],  users)
+    assert_equal([u'group_test_list_for_autocomplete', u'group_test_list_for_autocomplete_other_group'], groups)
 
   def test_language_preference(self):
     # Test that language selection appears in Edit Profile for current user

+ 34 - 27
apps/useradmin/src/useradmin/views.py

@@ -96,33 +96,40 @@ def list_configurations(request):
 
 
 def list_for_autocomplete(request):
-  if request.ajax:
-    extended_user_object = request.GET.get('extend_user') == 'true'
-
-    if request.user.is_superuser:
-      users = User.objects.all().order_by('username')
-      groups = Group.objects.all().order_by('name')
-      if request.GET.get('only_mygroups'):
-        groups = request.user.groups.all()
-    else:
-      usergroups = request.user.groups.all()
-      users = User.objects.filter(groups__in=usergroups).order_by('username').distinct()
-      groups = usergroups.order_by('name')
-
-    if not request.GET.get('include_myself'):
-      users = users.exclude(pk=request.user.pk)
-
-    users = users[:2000]
-    groups = groups[:2000]
-
-    response = {
-      'users': massage_users_for_json(users, extended_user_object),
-      'groups': massage_groups_for_json(groups)
-    }
-    return JsonResponse(response)
-
-  return HttpResponse("")
-
+  extended_user_object = request.GET.get('extend_user') == 'true'
+  autocomplete_filter = request.GET.get('filter', "")
+  if request.user.is_superuser:
+    users = User.objects.filter(username__icontains=autocomplete_filter).order_by('username')
+    groups = Group.objects.filter(name__icontains=autocomplete_filter).order_by('name')
+    if request.GET.get('only_mygroups'):
+      groups = request.user.groups.filter(name__icontains=autocomplete_filter).order_by('name')
+  else:
+    usergroups = request.user.groups.all()
+    # Get all users in the usergroups he belongs to and then filter by username
+    users = User.objects.filter(groups__in=usergroups, username__icontains=autocomplete_filter).order_by('username').distinct()
+    groups = usergroups.filter(name__icontains=autocomplete_filter).order_by('name')
+
+  # Don't include myself by default
+  if not request.GET.get('include_myself'):
+    users = users.exclude(pk=request.user.pk)
+
+  users = users[:100]
+  groups = groups[:100]
+
+  response = {
+    'users': massage_users_for_json(users, extended_user_object),
+    'groups': massage_groups_for_json(groups)
+  }
+  return JsonResponse(response)
+
+def get_users_by_id(request):
+  userids = json.loads(request.GET.get('userids', "[]"))
+  userids = userids[:100]
+  users = User.objects.filter(id__in=userids).order_by('username')
+  response = {
+    'users': massage_users_for_json(users)
+  }
+  return JsonResponse(response)
 
 def massage_users_for_json(users, extended=False):
   simple_users = []

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 24 - 1
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -364,7 +364,30 @@ var ApiHelper = (function () {
 
   ApiHelper.prototype.fetchUsersAndGroups = function (options) {
     var self = this;
-    self.simpleGet('/desktop/api/users/autocomplete', {}, options);
+    $.ajax({
+      method: "GET",
+      url: "/desktop/api/users/autocomplete",
+      data: options.data || {},
+      contentType: 'application/json'
+    }).done(function(response) {
+      options.successCallback(response);
+    }).fail(function (response) {
+      options.errorCallback(response);
+    });
+  };
+
+  ApiHelper.prototype.fetchUsersByIds = function (options) {
+    var self = this;
+    $.ajax({
+      method: "GET",
+      url: "/desktop/api/users",
+      data: { userids: options.userids},
+      contentType: 'application/json'
+    }).done(function(response) {
+      options.successCallback(response);
+    }).fail(function (response) {
+      options.errorCallback(response);
+    });
   };
 
   /**

+ 3 - 0
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -254,6 +254,9 @@
               ko.applyBindings(item.data, $li[0]);
             }
           },
+          _resizeMenu: function () {
+            this.menu.element.outerWidth(options.minWidth);
+          },
           _renderMenu: function (ul, items) {
             var self = this;
             hideSpinner();

+ 90 - 96
desktop/core/src/desktop/static/desktop/js/share2.vm.js

@@ -17,12 +17,19 @@
 
 var shareViewModel;
 
-function ShareViewModel(updateDocF) {
-  var self = this;
 
-  self.hasInitBeenCalled = false;
-  self.hasSetupBeenCalled = false;
+var errorCallback = function(response) {
+  $(document).trigger("error", "There was an error processing your action: " + response.responseText);
+};
 
+function ShareViewModel(updateDocF) {
+  var self = this;
+  self.userMap = {};
+  self.idToUserMap = {};
+  self.groupMap = {};
+  self.items = [];
+  self.apiHelper = ApiHelper.getInstance();
+  self.searchInput = ko.observable();
   self.selectedPerm = ko.observable('read');
   self.selectedPermLabel = ko.computed(function() {
     if (self.selectedPerm() == 'write') {
@@ -52,25 +59,38 @@ function ShareViewModel(updateDocF) {
   self.setDocUuid = function(docUuid) {
     if (docUuid === '') { return false; }
     self.docUuid = docUuid;
-    $.get('/desktop/api2/doc', { uuid : docUuid }, function (data) {
-      shareViewModel.selectedDoc(data.document)
-    }).fail(function (response) {
-      $(document).trigger("error", "There was an error processing your action: " + response.responseText);
+    var fetchDocumentsSuccessCallback = function (data) {
+      readusers = data.document.perms.read.users.map(function(user){return user.id});
+      writeusers = data.document.perms.write.users.map(function(user){return user.id});
+      allusers = readusers.concat(writeusers);
+      if (allusers.length > 0) {
+        var successCallback = function (response) {
+          $.each(response.users, function (i, user) {
+            // Needed for getting prettyusername of already shared users
+            shareViewModel.idToUserMap[user.id] = user;
+          });
+          shareViewModel.selectedDoc(data.document);
+        };
+        shareViewModel.apiHelper.fetchUsersByIds({
+          userids: JSON.stringify(allusers),
+          successCallback: successCallback,
+          errorCallback: errorCallback
+        });
+      } else {
+        shareViewModel.selectedDoc(data.document);
+      }
+    }
+    shareViewModel.apiHelper.fetchDocuments({
+      uuid: docUuid,
+      successCallback: fetchDocumentsSuccessCallback,
+      errorCallback: errorCallback
     });
   }
 }
 
-
 function openShareModal() {
   $("#documentShareModal").modal("show");
-  setupSharing(function(){
-    $("#documentShareAddBtn").removeClass("disabled");
-    $("#documentShareCaret").removeClass("disabled");
-    $("#documentShareTypeahead").removeAttr("disabled");
-    $(".notpretty").each(function(){
-      $(this).text(prettifyUsername($(this).attr("data-id")));
-    });
-  });
+  setupSharing();
 }
 
 function isShared() {
@@ -79,21 +99,19 @@ function isShared() {
   return read.users.length + read.groups.length > 0
 }
 
-function prettifyUsername(userId) {
-  var _user = null;
-  if (self.hasSetupBeenCalled) {
-    for (var i = 0; i < JSON_USERS_GROUPS.users.length; i++) {
-      if (JSON_USERS_GROUPS.users[i].id == userId) {
-        _user = JSON_USERS_GROUPS.users[i];
-      }
-    }
-    if (_user != null) {
-      return (_user.first_name != "" ? _user.first_name + " " : "") + (_user.last_name != "" ? _user.last_name + " " : "") + ((_user.first_name != "" || _user.last_name != "") ? "(" : "") + _user.username + ((_user.first_name != "" || _user.last_name != "") ? ")" : "");
-    }
+function prettifyUsernameById(id) {
+  return prettifyUsername(shareViewModel.idToUserMap[id]);
+}
+
+function prettifyUsername(user) {
+  if (user != null && user.hasOwnProperty("username")) {
+    return (user.first_name != "" ? user.first_name + " " : "") + (user.last_name != "" ? user.last_name + " " : "") + ((user.first_name != "" || user.last_name != "") ? "(" : "") + user.username + ((user.first_name != "" || user.last_name != "") ? ")" : "");
   }
+
   return "";
 }
 
+
 function initSharing(id, updateFunc) {
   if(! updateFunc) {
     updateFunc = function () {}
@@ -101,90 +119,64 @@ function initSharing(id, updateFunc) {
   shareViewModel = new ShareViewModel(updateFunc);
   ko.cleanNode($(id)[0]);
   ko.applyBindings(shareViewModel, $(id)[0]);
-  shareViewModel.hasInitBeenCalled = true;
   return shareViewModel;
 }
 
 function setupSharing(id, updateFunc) {
-  if (shareViewModel == null || !shareViewModel.hasInitBeenCalled) {
+  if (shareViewModel == null ) {
     shareViewModel = initSharing(id, updateFunc);
   }
 
-  if (! self.hasSetupBeenCalled){
-    $.getJSON('/desktop/api/users/autocomplete', function (data) {
-      self.hasSetupBeenCalled = true;
-      JSON_USERS_GROUPS = data;
-      dropdown = [];
-      usermap = {};
-      groupmap = {};
-
-      $.each(JSON_USERS_GROUPS.users, function (i, user) {
-        var _display = prettifyUsername(user.id);
-        usermap[_display] = user;
-        dropdown.push(_display);
-      });
+  $("#documentShareAddBtn").on("click", function () {
+    handleTypeaheadSelection();
+  });
 
-      $.each(JSON_USERS_GROUPS.groups, function (i, group) {
-        groupmap[group.name] = group;
-        dropdown.push(group.name);
-      });
+  return shareViewModel;
+}
 
-      $("#documentShareTypeahead").typeahead({
-        source: function (query, process) {
-          process(dropdown);
-        },
-        matcher: function (item) {
-          if (item.toLowerCase().indexOf(this.query.trim().toLowerCase()) != -1) {
-            return true;
-          }
-        },
-        sorter: function (items) {
-          return items.sort();
+function source(request, callback) {
+
+  var successCallback = function (data) {
+    JSON_USERS_GROUPS = data;
+    shareViewModel.items = [];
+    $.each(JSON_USERS_GROUPS.users, function (i, user) {
+      var label = prettifyUsername(user);
+      shareViewModel.userMap[label] = user;
+      shareViewModel.items.push({
+        data: {
+          "icon": "fa fa-user",
+          "label": label
         },
-        highlighter: function (item) {
-          var _icon = "fa";
-          if (usermap[item]) {
-            _icon += " fa-user";
-          }
-          else {
-            _icon += " fa-users";
-          }
-          var regex = new RegExp('(' + this.query + ')', 'gi');
-          return "<i class='" + _icon + "'></i> " + item.replace(regex, "<strong>$1</strong>");
+        value: label
+      });
+      shareViewModel.idToUserMap[user.id] = user;
+    });
+    $.each(JSON_USERS_GROUPS.groups, function (i, group) {
+      shareViewModel.groupMap[group.name] = group;
+      shareViewModel.items.push({
+        data: {
+          "icon": "fa fa-users",
+          "label": group.name
         },
-        updater: function (item) {
-          selectedUserOrGroup = usermap[item] ? usermap[item] : groupmap[item];
-          return item;
-        }
+        value: group.name
       });
+    });
 
-      $("#documentShareTypeahead").on("keyup", function (e) {
-        var _code = (e.keyCode ? e.keyCode : e.which);
-        if (_code == 13) {
-          handleTypeaheadSelection();
-        }
-      });
+    if(shareViewModel.items.length == 0){
+     shareViewModel.items.push({
+       'noMatch': true
+     });
+    }
 
-      if (typeof id == "function"){
-        id();
-      }
-    }).fail(function (response) {
-      $(document).trigger("error", "There was an error processing your action: " + response.responseText);
-    });
+    callback(shareViewModel.items);
 
-    $("#documentShareAddBtn").on("click", function () {
-      if (! $(this).hasClass("disabled")){
-        handleTypeaheadSelection();  
-      }
-    });
-  }
-  else {
-    if (typeof id == "function"){
-      id();
     }
-  }
 
-  return shareViewModel;
+  shareViewModel.apiHelper.fetchUsersAndGroups({
+    data: {filter: request.term},
+    successCallback: successCallback,
+    errorCallback: errorCallback
+  });
 }
 
 function updateSharePerm(perms, user) {
@@ -218,6 +210,8 @@ function changeDocumentSharePerm(perm) {
 }
 
 function handleTypeaheadSelection() {
+  searchAutoCompInput = $("#userSearchAutocomp").val();
+  selectedUserOrGroup = shareViewModel.userMap[searchAutoCompInput] ? shareViewModel.userMap[searchAutoCompInput] : shareViewModel.groupMap[searchAutoCompInput];
   if (selectedUserOrGroup != null) {
     if (selectedUserOrGroup.hasOwnProperty("username")) {
       shareViewModel.selectedDoc().perms[shareViewModel.selectedPerm()].users.push(selectedUserOrGroup);
@@ -229,7 +223,7 @@ function handleTypeaheadSelection() {
     shareDocFinal();
   }
   selectedUserOrGroup = null;
-  $("#documentShareTypeahead").val("");
+  $("#userSearchAutocomp").val('');
 }
 
 function shareDocFinal() {

+ 21 - 0
desktop/core/src/desktop/static/desktop/less/hue-cross-version.less

@@ -598,3 +598,24 @@ ul.errorlist {
     }
   }
 }
+
+.hue-autocomplete-item:hover > a,
+.hue-autocomplete-item:focus > a {
+  background-color: @cui-blue-700 !important;
+}
+
+.hue-autocomplete {
+  border-radius: @hue-panel-border-radius;
+}
+
+.hue-autocomplete-item {
+  a {
+    height: 40px;
+    padding-left: 20px !important;
+    padding-top: 10px !important;
+    span{
+      font-size: 15px;
+      font-weight: bold;
+    }
+  }
+}

+ 21 - 7
desktop/core/src/desktop/templates/common_share.mako

@@ -17,6 +17,22 @@
 from django.utils.translation import ugettext as _
 %>
 
+<script type="text/html" id="user-search-autocomp-item">
+  <a>
+    <div>
+      <i data-bind="css: icon"></i>
+      <span data-bind="html: label"></span>
+    </div>
+  </a>
+</script>
+
+<script type="text/html" id="user-search-autocomp-no-match">
+   <div>
+     <div>${ _('No match found') }</div>
+   </div>
+ </script>
+
+
 <div id="documentShareModal" class="modal hide fade">
   <!-- ko if: selectedDoc() -->
   <div class="modal-header">
@@ -29,32 +45,30 @@ from django.utils.translation import ugettext as _
         <h4 class="muted" style="margin-top:0px">${_('Read')}</h4>
         <div data-bind="visible: (selectedDoc().perms.read.users.length == 0 && selectedDoc().perms.read.groups.length == 0)">${_('The document is not shared for read.')}</div>
         <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.read.users">
-          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsername(id), css:{'notpretty': prettifyUsername(id) == ''}, attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserReadShare"> <i class="fa fa-times"></i></span></li>
+          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsernameById(id), attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserReadShare"> <i class="fa fa-times"></i></span></li>
         </ul>
         <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.read.groups">
           <li><span class="badge badge-info badge-left"><i class="fa fa-users"></i> ${ _('Group') } <span data-bind="text: name"></span></span><span class="badge badge-right trash-share" data-bind="click: removeGroupReadShare"> <i class="fa fa-times"></i></span></li>
         </ul>
       </div>
-
       <div class="span6">
         <h4 class="muted" style="margin-top:0px">${_('Read and Modify')}</h4>
         <div data-bind="visible: (selectedDoc().perms.write.users.length == 0 && selectedDoc().perms.write.groups.length == 0)">${_('The document is not shared for read and modify.')}</div>
         <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.write.users">
-          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsername(id), css:{'notpretty': prettifyUsername(id) == ''}, attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserWriteShare"> <i class="fa fa-times"></i></span></li>
+          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsernameById(id), attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserWriteShare"> <i class="fa fa-times"></i></span></li>
         </ul>
         <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.write.groups">
           <li><span class="badge badge-info badge-left"><i class="fa fa-users"></i> ${ _('Group') } <span data-bind="text: name"></span></span><span class="badge badge-right trash-share" data-bind="click: removeGroupWriteShare"> <i class="fa fa-times"></i></span></li>
         </ul>
       </div>
-
     </div>
     <div class="clearfix"></div>
     <div style="margin-top: 20px">
       <div class="input-append">
-        <input id="documentShareTypeahead" type="text" style="width: 460px" placeholder="${_('Type a username or a group name')}" disabled="disabled">
+      <input id="userSearchAutocomp" placeholder="${_('Type a username or a group name')}" type="text" data-bind="autocomplete: { source: source, itemTemplate: 'user-search-autocomp-item', noMatchTemplate: 'user-search-autocomp-no-match',valueObservable: searchInput, showSpinner: true, classPrefix: 'hue-', limitWidthToInput: true, minWidth: 160, onEnter: handleTypeaheadSelection }, clearable: { value: searchInput }, textInput: searchInput" class="ui-autocomplete-input" autocomplete="off" style="width: 460px">
         <div class="btn-group">
-          <a id="documentShareAddBtn" class="btn disabled"><i class="fa fa-plus-circle"></i> <span data-bind="text: selectedPermLabel"></span></a>
-          <a id="documentShareCaret" class="btn dropdown-toggle disabled" data-toggle="dropdown">
+          <a id="documentShareAddBtn" class="btn"><i class="fa fa-plus-circle"></i> <span data-bind="text: selectedPermLabel"></span></a>
+          <a id="documentShareCaret" class="btn dropdown-toggle" data-toggle="dropdown">
             <span class="caret"></span>
           </a>
           <ul class="dropdown-menu pull-right">

+ 1 - 0
desktop/core/src/desktop/urls.py

@@ -169,6 +169,7 @@ dynamic_patterns += patterns('desktop.configuration.api',
 
 dynamic_patterns += patterns('useradmin.views',
   (r'^desktop/api/users/autocomplete', 'list_for_autocomplete'),
+  (r'^desktop/api/users/?$', 'get_users_by_id'),
 )
 
 dynamic_patterns += patterns('desktop.lib.vcs.api',

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels