瀏覽代碼

HUE-1051 [useradmin] Bulk delete users

Romain Rigaux 12 年之前
父節點
當前提交
7a8e442bf3

+ 2 - 1
apps/beeswax/src/beeswax/templates/show_tables.mako

@@ -72,7 +72,8 @@ ${layout.menubar(section='tables')}
                       <div class="hueCheckbox tableCheck"
                            data-view-url="${ url(app_name + ':describe_table', database=database, table=table) }"
                            data-browse-url="${ url(app_name + ':read_table', database=database, table=table) }"
-                           data-drop-name="${ table }" data-row-selector-exclude="true"></div>
+                           data-drop-name="${ table }"
+                           data-row-selector-exclude="true"></div>
                     </td>
                     <td>
                       <a href="${ url(app_name + ':describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>

+ 0 - 1
apps/beeswax/src/beeswax/views.py

@@ -303,7 +303,6 @@ def show_tables(request, database=None):
 
   tables = db.get_tables(database=database)
   examples_installed = beeswax.models.MetaInstall.get().installed_example
-  #table_selection = TableSelection(tables=tables)
 
   return render("show_tables.mako", request, {
       'tables': tables,

+ 0 - 28
apps/useradmin/src/useradmin/templates/delete_user.mako

@@ -1,28 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-<%!
-from django.utils.translation import ugettext as _
-%>
-<form id="deleteUserForm" action="${path}" method="POST">
-    <div class="modal-header">
-        <a href="#" class="close" data-dismiss="modal">&times;</a>
-        <h3 id="deleteUserMessage">${_("Are you sure you want to delete ")} ${username}${_("?")}</h3>
-    </div>
-    <div class="modal-footer">
-        <a href="#" class="btn" data-dismiss="modal">${_('No')}</a>
-        <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
-    </div>
-</form>

+ 102 - 69
apps/useradmin/src/useradmin/templates/list_users.mako

@@ -31,7 +31,7 @@ ${layout.menubar(section='users', _=_)}
     <%actionbar:render>
         <%def name="actions()">
             %if user.is_superuser:
-                <button id="deleteUserBtn" class="btn confirmationModal" title="${_('Delete')}" disabled="disabled"><i class="icon-trash"></i> ${_('Delete')}</button>
+                <button id="deleteUserBtn" class="btn" title="${_('Delete')}" disabled="disabled"><i class="icon-trash"></i> ${_('Delete')}</button>
             %endif
         </%def>
         <%def name="creation()">
@@ -62,7 +62,7 @@ ${layout.menubar(section='users', _=_)}
             <tr class="tableRow" data-search="${listed_user.username}${listed_user.first_name}${listed_user.last_name}${listed_user.email}${', '.join([group.name for group in listed_user.groups.all()])}">
                 %if user.is_superuser:
                     <td data-row-selector-exclude="true">
-                        <div class="hueCheckbox userCheck" data-username="${listed_user.username}" data-confirmation-url="${ url('useradmin.views.delete_user', username=urllib.quote(listed_user.username))}" data-row-selector-exclude="true"></div>
+                        <div class="hueCheckbox userCheck" data-row-selector-exclude="true" data-id="${ listed_user.id }"></div>
                     </td>
                 %endif
                 <td>
@@ -93,85 +93,118 @@ ${layout.menubar(section='users', _=_)}
 
     <div id="syncLdap" class="modal hide fade"></div>
 
-    <div id="deleteUser" class="modal hide fade"></div>
+    <div id="deleteUser" class="modal hide fade">
+        <form id="dropTableForm" action="${ url('useradmin.views.delete_user') }" method="POST">
+            <div class="modal-header">
+                <a href="#" class="close" data-dismiss="modal">&times;</a>
+                <h3 id="deleteUserMessage">${ _("Are you sure you want to delete the selected user(s)?") }</h3>
+            </div>
+            <div class="modal-footer">
+                <a href="#" class="btn" data-dismiss="modal">${_('No')}</a>
+                <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
+            </div>
+            <div class="hide">
+                <select name="user_ids" data-bind="options: availableUsers, selectedOptions: chosenUsers" multiple="true"></select>
+            </div>
+        </form>
+    </div>
 
 </div>
 
-<script type="text/javascript" charset="utf-8">
-    $(document).ready(function(){
-        $(".datatables").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bInfo": false,
-            "bFilter": false,
-            "aoColumns": [
-                %if user.is_superuser:
-                { "bSortable": false },
-                %endif
-                null,
-                null,
-                null,
-                null,
-                null,
-                { "sType": "date" }
-            ],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}",
-            }
-        });
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 
-        $(".dataTables_wrapper").css("min-height","0");
-        $(".dataTables_filter").hide();
-
-        $(".confirmationModal").click(function(){
-            var _this = $(this);
-            $.ajax({
-                url: _this.data("confirmation-url"),
-                beforeSend: function(xhr){
-                    xhr.setRequestHeader("X-Requested-With", "Hue");
-                },
-                dataType: "html",
-                success: function(data){
-                    $("#deleteUser").html(data);
-                    $("#deleteUser").modal("show");
-                }
-            });
-        });
+<script type="text/javascript" charset="utf-8">
+  $(document).ready(function () {
+    var viewModel = {
+        availableUsers : ko.observableArray(${ users_json | n }),
+        chosenUsers : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
+    $(".datatables").dataTable({
+        "bPaginate": false,
+        "bLengthChange": false,
+        "bInfo": false,
+        "bFilter": false,
+        "aoColumns": [
+            %if user.is_superuser:
+            { "bSortable": false },
+            %endif
+            null,
+            null,
+            null,
+            null,
+            null,
+            { "sType": "date" }
+        ],
+        "oLanguage": {
+            "sEmptyTable": "${_('No data available')}",
+            "sZeroRecords": "${_('No matching records')}",
+        }
+    });
 
-        $("#selectAll").click(function(){
-            if ($(this).attr("checked")) {
-                $(this).removeAttr("checked");
-                $(".userCheck").removeClass("icon-ok").removeAttr("checked");
-            }
-            else {
-                $(this).attr("checked", "checked");
-                $(".userCheck").addClass("icon-ok").attr("checked", "checked");
+    $(".dataTables_wrapper").css("min-height","0");
+    $(".dataTables_filter").hide();
+
+    $(".confirmationModal").click(function(){
+        var _this = $(this);
+        $.ajax({
+            url: _this.data("confirmation-url"),
+            beforeSend: function(xhr){
+                xhr.setRequestHeader("X-Requested-With", "Hue");
+            },
+            dataType: "html",
+            success: function(data){
+                $("#deleteUser").html(data);
+                $("#deleteUser").modal("show");
             }
-            toggleActions();
         });
+    });
 
-        $(".userCheck").click(function(){
-            if ($(this).attr("checked")) {
-                $(this).removeClass("icon-ok").removeAttr("checked");
-            }
-            else {
-                $(this).addClass("icon-ok").attr("checked", "checked");
-            }
-            toggleActions();
-        });
+    $("#selectAll").click(function(){
+        if ($(this).attr("checked")) {
+            $(this).removeAttr("checked");
+            $(".userCheck").removeClass("icon-ok").removeAttr("checked");
+        }
+        else {
+            $(this).attr("checked", "checked");
+            $(".userCheck").addClass("icon-ok").attr("checked", "checked");
+        }
+        toggleActions();
+    });
 
-        function toggleActions() {
-            if ($(".userCheck[checked='checked']").length == 1) {
-                $("#deleteUserBtn").removeAttr("disabled").data("confirmation-url", $(".userCheck[checked='checked']").data("confirmation-url"));
-            }
-            else {
-                $("#deleteUserBtn").attr("disabled", "disabled");
-            }
+    $(".userCheck").click(function(){
+        if ($(this).attr("checked")) {
+            $(this).removeClass("icon-ok").removeAttr("checked");
         }
+        else {
+            $(this).addClass("icon-ok").attr("checked", "checked");
+        }
+        toggleActions();
+    });
 
-        $("a[data-row-selector='true']").jHueRowSelector();
+    function toggleActions() {
+        if ($(".userCheck[checked='checked']").length >= 1) {
+            $("#deleteUserBtn").removeAttr("disabled");
+        }
+        else {
+            $("#deleteUserBtn").attr("disabled", "disabled");
+        }
+    }
+
+    $("#deleteUserBtn").click(function () {
+        viewModel.chosenUsers.removeAll();
+
+        $(".hueCheckbox[checked='checked']").each(function( index ) {
+            viewModel.chosenUsers.push($(this).data("id"));
+        });
+
+        $("#deleteUser").modal("show");
     });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
 </script>
 
 ${layout.commons()}

+ 14 - 5
apps/useradmin/src/useradmin/tests.py

@@ -264,6 +264,7 @@ def test_user_admin():
   useradmin.conf.DEFAULT_USER_GROUP.set_for_testing('test_default')
 
   c = make_logged_in_client('test', is_superuser=True)
+  user = User.objects.get(username='test')
 
   # Test basic output.
   response = c.get('/useradmin/')
@@ -294,7 +295,7 @@ def test_user_admin():
   assert_true("You cannot remove" in response.content,
               "Shouldn't be able to remove the last superuser")
   # Shouldn't be able to delete oneself
-  response = c.post('/useradmin/users/delete/test', {})
+  response = c.post('/useradmin/users/delete', {u'user_ids': [user.id]})
   assert_true("You cannot remove yourself" in response.content,
               "Shouldn't be able to delete the last superuser")
 
@@ -357,8 +358,9 @@ def test_user_admin():
                              is_active = True))
   response = c_reg.get('/useradmin/users/edit/%s' % (FUNNY_NAME_QUOTED,))
   assert_equal("Hello", response.context["form"].instance.first_name)
+  funny_user = User.objects.get(username=FUNNY_NAME)
   # Can't edit other people.
-  response = c_reg.post("/useradmin/users/delete/test")
+  response = c_reg.post("/useradmin/users/delete", {u'user_ids': [funny_user.id]})
   assert_true("You must be a superuser" in response.content,
               "Regular user can't edit other people")
 
@@ -376,16 +378,23 @@ def test_user_admin():
 
   # Delete that regular user
   funny_profile = get_profile(test_user)
-  response = c_su.post('/useradmin/users/delete/%s' % (FUNNY_NAME_QUOTED,))
+  response = c_su.post('/useradmin/users/delete', {u'user_ids': [funny_user.id]})
   assert_equal(302, response.status_code)
   assert_false(User.objects.filter(username=FUNNY_NAME).exists())
   assert_false(UserProfile.objects.filter(id=funny_profile.id).exists())
 
+  # Bulk delete users
+  u1 = User.objects.create(username='u1', password="u1")
+  u2 = User.objects.create(username='u2', password="u2")
+  assert_equal(User.objects.filter(username__in=['u1', 'u2']).count(), 2)
+  response = c_su.post('/useradmin/users/delete', {u'user_ids': [u1.id, u2.id]})
+  assert_equal(User.objects.filter(username__in=['u1', 'u2']).count(), 0)
+
   # Make sure that user deletion works if the user has never performed a request.
-  User.objects.create(username=FUNNY_NAME, password='test')
+  funny_user = User.objects.create(username=FUNNY_NAME, password='test')
   assert_true(User.objects.filter(username=FUNNY_NAME).exists())
   assert_false(UserProfile.objects.filter(user__username=FUNNY_NAME).exists())
-  response = c_su.post('/useradmin/users/delete/%s' % (FUNNY_NAME_QUOTED,))
+  response = c_su.post('/useradmin/users/delete', {u'user_ids': [funny_user.id]})
   assert_equal(302, response.status_code)
   assert_false(User.objects.filter(username=FUNNY_NAME).exists())
   assert_false(UserProfile.objects.filter(user__username=FUNNY_NAME).exists())

+ 1 - 1
apps/useradmin/src/useradmin/urls.py

@@ -34,6 +34,6 @@ urlpatterns = patterns('useradmin.views',
   url(r'^permissions/edit/(?P<app>.*)/(?P<priv>.*)$', 'edit_permission'),
   url(r'^users/new$', 'edit_user', name="useradmin.new"),
   url(r'^groups/new$', 'edit_group', name="useradmin.new_group"),
-  url(r'^users/delete/(?P<username>%s)$' % (username_re,), 'delete_user'),
+  url(r'^users/delete', 'delete_user'),
   url(r'^groups/delete/(?P<name>%s)$' % (groupname_re,), 'delete_group'),
 )

+ 26 - 25
apps/useradmin/src/useradmin/views.py

@@ -24,6 +24,10 @@ import grp
 import logging
 import threading
 import subprocess
+try:
+  import json
+except ImportError:
+  import simplejson as json
 
 import ldap_access
 from ldap import LDAPError
@@ -52,7 +56,11 @@ __groups_lock = threading.Lock()
 
 
 def list_users(request):
-  return render("list_users.mako", request, dict(users=User.objects.all(), request=request))
+  return render("list_users.mako", request, {
+      'users': User.objects.all(),
+      'users_json': json.dumps(list(User.objects.values_list('id', flat=True))),
+      'request': request
+  })
 
 
 def list_groups(request):
@@ -63,34 +71,27 @@ def list_permissions(request):
   return render("list_permissions.mako", request, dict(permissions=HuePermission.objects.all()))
 
 
-def delete_user(request, username):
+def delete_user(request):
   if not request.user.is_superuser:
     raise PopupException(_("You must be a superuser to delete users."), error_code=401)
 
-  if request.method == 'POST':
-    try:
-      global __users_lock
-      __users_lock.acquire()
-      try:
-        if username == request.user.username:
-          raise PopupException(_("You cannot remove yourself."), error_code=401)
-        user = User.objects.get(username=username)
-        # Since profiles are lazily created, they may not exist.
-        try:
-          user_profile = UserProfile.objects.get(user=user)
-          user_profile.delete()
-        except UserProfile.DoesNotExist, e:
-          pass
-        user.delete()
-      finally:
-        __users_lock.release()
+  if request.method != 'POST':
+    raise PopupException(_('A POST request is required.'))
 
-      request.info(_('The user was deleted.'))
-      return redirect(reverse(list_users))
-    except User.DoesNotExist:
-      raise PopupException(_("User not found."), error_code=404)
-  else:
-    return render("delete_user.mako", request, dict(path=request.path, username=username))
+  ids = request.POST.getlist('user_ids')
+  global __users_lock
+  __users_lock.acquire()
+  try:
+    if str(request.user.id) in ids:
+      raise PopupException(_("You cannot remove yourself."), error_code=401)
+
+    UserProfile.objects.filter(user__id__in=ids).delete()
+    User.objects.filter(id__in=ids).delete()
+  finally:
+    __users_lock.release()
+
+  request.info(_('The users were deleted.'))
+  return redirect(reverse(list_users))
 
 
 def delete_group(request, name):