浏览代码

HUE-957 [beeswax] Bulk UI operations

Bulk delete drop tables
Bulk delete drop designs
Move all bulk operations to POST
On confirmation popup, fillup the form select with the list of objects
to delete. On submit retrieved the list from the POST data.
Romain Rigaux 13 年之前
父节点
当前提交
6adafb050f

+ 13 - 2
apps/beeswax/src/beeswax/models.py

@@ -30,7 +30,7 @@ from enum import Enum
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
 
 
 from beeswax.conf import SERVER_INTERFACE
 from beeswax.conf import SERVER_INTERFACE
-from beeswax.design import HQLdesign
+from beeswax.design import HQLdesign, hql_query
 from beeswaxd.ttypes import QueryHandle as BeeswaxdQueryHandle, QueryState
 from beeswaxd.ttypes import QueryHandle as BeeswaxdQueryHandle, QueryState
 from cli_service.ttypes import TSessionHandle, THandleIdentifier,\
 from cli_service.ttypes import TSessionHandle, THandleIdentifier,\
   TOperationState, TOperationHandle, TOperationType
   TOperationState, TOperationHandle, TOperationType
@@ -72,7 +72,7 @@ class QueryHistory(models.Model):
   server_name = models.CharField(max_length=128, help_text=_('Name of the query server.'), default='')
   server_name = models.CharField(max_length=128, help_text=_('Name of the query server.'), default='')
   server_type = models.CharField(max_length=128, help_text=_('Type of the query server.'), default=BEESWAX, choices=SERVER_TYPE)
   server_type = models.CharField(max_length=128, help_text=_('Type of the query server.'), default=BEESWAX, choices=SERVER_TYPE)
 
 
-  design = models.ForeignKey('SavedQuery', to_field='id', null=True) # Some queries (like read/drop table) don't have a design
+  design = models.ForeignKey('SavedQuery', to_field='id', null=True) # Some queries (like read/create table) don't have a design
   notify = models.BooleanField(default=False)                        # Notify on completion
   notify = models.BooleanField(default=False)                        # Notify on completion
 
 
   class Meta:
   class Meta:
@@ -279,6 +279,17 @@ class SavedQuery(models.Model):
     design.is_auto = copy.deepcopy(self.is_auto)
     design.is_auto = copy.deepcopy(self.is_auto)
     return design
     return design
 
 
+  @classmethod
+  def create_empty(cls, app_name, owner):
+    query_type = SavedQuery.TYPES_MAPPING[app_name]
+    design = SavedQuery(owner=owner, type=query_type)
+    design.name = SavedQuery.DEFAULT_NEW_DESIGN_NAME
+    design.desc = ''
+    design.data = hql_query('').dumps()
+    design.is_auto = True
+    design.save()
+    return design
+
   @staticmethod
   @staticmethod
   def get(id, owner=None, type=None):
   def get(id, owner=None, type=None):
     """
     """

+ 17 - 0
apps/beeswax/src/beeswax/server/dbms.py

@@ -141,6 +141,21 @@ class Dbms:
     return self.execute_statement(hql)
     return self.execute_statement(hql)
 
 
 
 
+  def drop_tables(self, database, tables, design):
+    hql = []
+
+    for table in tables:
+      if table.is_view:
+        hql.append("DROP VIEW `%s.%s`" % (database, table.name,))
+      else:
+        hql.append("DROP TABLE `%s.%s`" % (database, table.name,))
+    query = hql_query(';'.join(hql), database)
+    design.data = query.dumps()
+    design.save()
+
+    return self.execute_query(query, design)
+
+
   def use(self, database):
   def use(self, database):
     """Beeswax does not support use directly."""
     """Beeswax does not support use directly."""
     if SERVER_INTERFACE.get() == HIVE_SERVER2:
     if SERVER_INTERFACE.get() == HIVE_SERVER2:
@@ -174,6 +189,7 @@ class Dbms:
       curr = time.time()
       curr = time.time()
     return None
     return None
 
 
+
   def execute_next_statement(self, query_history):
   def execute_next_statement(self, query_history):
     query_history.statement_number += 1
     query_history.statement_number += 1
     query_history.last_state = QueryHistory.STATE.submitted.index
     query_history.last_state = QueryHistory.STATE.submitted.index
@@ -181,6 +197,7 @@ class Dbms:
     query = query_history.design.get_design()
     query = query_history.design.get_design()
     return self.execute_and_watch(query, query_history=query_history)
     return self.execute_and_watch(query, query_history=query_history)
 
 
+
   def execute_and_watch(self, query, design=None, query_history=None):
   def execute_and_watch(self, query, design=None, query_history=None):
     """
     """
     Run query and return a QueryHistory object in order to see its progress on a Web page.
     Run query and return a QueryHistory object in order to see its progress on a Web page.

+ 7 - 2
apps/beeswax/src/beeswax/templates/describe_table.mako

@@ -131,7 +131,7 @@ ${layout.menubar(section='tables')}
 
 
 
 
 <div id="dropTable" class="modal hide fade">
 <div id="dropTable" class="modal hide fade">
-    <form id="dropTableForm" method="POST" action="${ url(app_name + ':drop_table', database=database, table=table.name) }">
+    <form id="dropTableForm" method="POST" action="${ url(app_name + ':drop_table', database=database) }">
     <div class="modal-header">
     <div class="modal-header">
         <a href="#" class="close" data-dismiss="modal">&times;</a>
         <a href="#" class="close" data-dismiss="modal">&times;</a>
         <h3>${_('Drop Table')}</h3>
         <h3>${_('Drop Table')}</h3>
@@ -144,6 +144,11 @@ ${layout.menubar(section='tables')}
         <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
         <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
         <input type="submit" class="btn btn-danger" value="${_('Yes, drop this table')}"/>
         <input type="submit" class="btn btn-danger" value="${_('Yes, drop this table')}"/>
     </div>
     </div>
+    <div class="hide">
+      <select name="table_selection">
+        <option value="${ table.name }" selected>${ table.name }</option>
+      </select>
+    </div>
     </form>
     </form>
 </div>
 </div>
 
 
@@ -250,7 +255,7 @@ ${layout.menubar(section='tables')}
        }
        }
      });
      });
 
 
-     $.getJSON("${ url(app_name + ':drop_table', database=database, table=table.name) }", function(data) {
+     $.getJSON("${ url(app_name + ':drop_table', database=database) }", function(data) {
        $("#dropTableMessage").text(data.title);
        $("#dropTableMessage").text(data.title);
      });
      });
 
 

+ 26 - 12
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -19,6 +19,7 @@
     from desktop.views import commonheader, commonfooter
     from desktop.views import commonheader, commonfooter
     from django.utils.translation import ugettext as _
     from django.utils.translation import ugettext as _
 %>
 %>
+
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
@@ -67,7 +68,7 @@ ${layout.menubar(section='saved queries')}
           <div class="hueCheckbox savedCheck"
           <div class="hueCheckbox savedCheck"
             % if may_edit:
             % if may_edit:
               data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
               data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
-              data-delete-url="${ url(app_name + ':delete_design', design_id=design.id) }"
+              data-delete-name="${ design.id }"
               data-history-url="${ url(app_name + ':list_query_history') }?design_id=${design.id}"
               data-history-url="${ url(app_name + ':list_query_history') }?design_id=${design.id}"
             % endif
             % endif
             data-clone-url="${ url(app_name + ':clone_design', design_id=design.id) }" data-row-selector-exclude="true"></div>
             data-clone-url="${ url(app_name + ':clone_design', design_id=design.id) }" data-row-selector-exclude="true"></div>
@@ -97,7 +98,7 @@ ${layout.menubar(section='saved queries')}
 </div>
 </div>
 
 
 <div id="deleteQuery" class="modal hide fade">
 <div id="deleteQuery" class="modal hide fade">
-  <form id="deleteQueryForm" action="" method="POST">
+  <form id="deleteQueryForm" action="${ url(app_name + ':delete_design') }" method="POST">
     <div class="modal-header">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
@@ -106,11 +107,23 @@ ${layout.menubar(section='saved queries')}
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
     </div>
+    <div class="hide">
+      <select name="designs_selection" data-bind="options: availableSavedQueries, selectedOptions: chosenSavedQueries" multiple="true"></select>
+    </div>
   </form>
   </form>
 </div>
 </div>
 
 
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function () {
   $(document).ready(function () {
+    var viewModel = {
+        availableSavedQueries : ko.observableArray(${ designs_json | n }),
+        chosenSavedQueries : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
     var savedQueries = $(".datatables").dataTable({
     var savedQueries = $(".datatables").dataTable({
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "bPaginate":false,
       "bPaginate":false,
@@ -179,20 +192,21 @@ ${layout.menubar(section='saved queries')}
             location.href = selector.data("history-url")
             location.href = selector.data("history-url")
           });
           });
         }
         }
-        if (selector.data("delete-url")) {
-          $("#deleteBtn").removeAttr("disabled").data("confirmation-url", selector.data("delete-url"));
-        }
+      }
+      if (selector.length >= 1) {
+        $("#deleteBtn").removeAttr("disabled");
       }
       }
     }
     }
 
 
     $("#deleteBtn").click(function () {
     $("#deleteBtn").click(function () {
-      if ($(this).data("confirmation-url")) {
-        $.getJSON($(this).data("confirmation-url"), function (data) {
-          $("#deleteQueryForm").attr("action", data.url);
-          $("#deleteQueryMessage").text(data.title);
-        });
-        $("#deleteQuery").modal("show");
-      }
+      $.getJSON("${ url(app_name + ':delete_design') }", function(data) {
+        $("#deleteQueryMessage").text(data.title);
+      });
+      viewModel.chosenSavedQueries.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenSavedQueries.push($(this).data("delete-name"));
+      });
+      $("#deleteQuery").modal("show");
     });
     });
 
 
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();

+ 1 - 3
apps/beeswax/src/beeswax/templates/list_history.mako

@@ -17,6 +17,7 @@
 import time
 import time
 from desktop.views import commonheader, commonfooter
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
+from beeswax import models
 from beeswax.views import collapse_whitespace
 from beeswax.views import collapse_whitespace
 %>
 %>
 
 
@@ -100,9 +101,6 @@ ${ layout.menubar(section='history') }
               </tr>
               </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
-            <%!
-              from beeswax import models, views
-            %>
             % for query in page.object_list:
             % for query in page.object_list:
               <%
               <%
                 qcontext = query.design.get_query_context()
                 qcontext = query.design.get_query_context()

+ 37 - 28
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -18,11 +18,13 @@ import time
 from django.template.defaultfilters import timesince
 from django.template.defaultfilters import timesince
 from desktop.views import commonheader, commonfooter
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
+from beeswax import models
+from beeswax.views import collapse_whitespace
 %>
 %>
+
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
-<%!  from beeswax.views import collapse_whitespace %>
 
 
 ${ commonheader(_('My Queries'), app_name, user, '100px') | n,unicode }
 ${ commonheader(_('My Queries'), app_name, user, '100px') | n,unicode }
 ${layout.menubar(section='my queries')}
 ${layout.menubar(section='my queries')}
@@ -68,14 +70,15 @@ ${layout.menubar(section='my queries')}
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-        <%!
-          from beeswax import models
-        %>
         % for design in q_page.object_list:
         % for design in q_page.object_list:
           <tr>
           <tr>
             <td data-row-selector-exclude="true">
             <td data-row-selector-exclude="true">
-              <div class="hueCheckbox savedCheck" data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }" data-delete-url="${ url(app_name + ':delete_design', design_id=design.id) }"
-                   data-history-url="${ url(app_name + ':list_query_history') }?design_id=${design.id}" data-clone-url="${ url(app_name + ':clone_design', design_id=design.id) }" data-row-selector-exclude="true"></div>
+              <div class="hueCheckbox savedCheck canDelete"
+                   data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
+                   data-delete-name="${ design.id }"
+                   data-history-url="${ url(app_name + ':list_query_history') }?design_id=${design.id}"
+                   data-clone-url="${ url(app_name + ':clone_design', design_id=design.id) }"
+                   data-row-selector-exclude="true"></div>
             </td>
             </td>
             <td>
             <td>
               <a href="${ url(app_name + ':execute_query', design_id=design.id) }" data-row-selector="true">${design.name}</a>
               <a href="${ url(app_name + ':execute_query', design_id=design.id) }" data-row-selector="true">${design.name}</a>
@@ -110,21 +113,14 @@ ${layout.menubar(section='my queries')}
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-        <%!
-          from beeswax import models, views
-        %>
         % for query in h_page.object_list:
         % for query in h_page.object_list:
         <%
         <%
-          qcontext = ""
-          try:
-            design = query.design
-            qcontext = views.make_query_context('design', design.id)
-          except:
-            pass
+          qcontext = query.design.get_query_context()
         %>
         %>
           <tr>
           <tr>
             <td width="1%" data-row-selector-exclude="true">
             <td width="1%" data-row-selector-exclude="true">
-              <div class="hueCheckbox runCheck" data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
+              <div class="hueCheckbox runCheck"
+                data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
                 % if qcontext and query.last_state != models.QueryHistory.STATE.expired.index:
                 % if qcontext and query.last_state != models.QueryHistory.STATE.expired.index:
                   data-view-url="${ url(app_name + ':watch_query', id=query.id) }?context=${qcontext|u}"
                   data-view-url="${ url(app_name + ':watch_query', id=query.id) }?context=${qcontext|u}"
                 % endif
                 % endif
@@ -152,7 +148,7 @@ ${layout.menubar(section='my queries')}
 </div>
 </div>
 
 
 <div id="deleteQuery" class="modal hide fade">
 <div id="deleteQuery" class="modal hide fade">
-  <form id="deleteQueryForm" action="" method="POST">
+  <form id="deleteQueryForm" action="${ url(app_name + ':delete_design') }" method="POST">
     <div class="modal-header">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
@@ -161,11 +157,22 @@ ${layout.menubar(section='my queries')}
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}"/>
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}"/>
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
     </div>
+    <div class="hide">
+      <select name="designs_selection" data-bind="options: availableSavedQueries, selectedOptions: chosenSavedQueries" multiple="true"></select>
+    </div>
   </form>
   </form>
 </div>
 </div>
 
 
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function () {
   $(document).ready(function () {
+    var viewModel = {
+        availableSavedQueries : ko.observableArray(${ designs_json | n }),
+        chosenSavedQueries : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
 
 
     updateQueryCounters();
     updateQueryCounters();
 
 
@@ -175,7 +182,7 @@ ${layout.menubar(section='my queries')}
       "bLengthChange":false,
       "bLengthChange":false,
       "bInfo":false,
       "bInfo":false,
       "aaSorting":[
       "aaSorting":[
-        [4, 'desc']
+        [1, 'asc']
       ],
       ],
       "aoColumns":[
       "aoColumns":[
         {"bSortable":false, "sWidth":"1%" },
         {"bSortable":false, "sWidth":"1%" },
@@ -272,9 +279,10 @@ ${layout.menubar(section='my queries')}
             location.href = selector.data("history-url")
             location.href = selector.data("history-url")
           });
           });
         }
         }
-        if (selector.data("delete-url")) {
-          $("#deleteBtn").removeAttr("disabled").data("confirmation-url", selector.data("delete-url"));
-        }
+      }
+
+      if (selector.length >= 1 && $('#recentSavedQueries').hasClass('active')) {
+        $("#deleteBtn").removeAttr("disabled");
       }
       }
     }
     }
 
 
@@ -290,13 +298,14 @@ ${layout.menubar(section='my queries')}
     }
     }
 
 
     $("#deleteBtn").click(function () {
     $("#deleteBtn").click(function () {
-      if ($(this).data("confirmation-url")) {
-        $.getJSON($(this).data("confirmation-url"), function (data) {
-          $("#deleteQueryForm").attr("action", data.url);
-          $("#deleteQueryMessage").text(data.title);
-        });
-        $("#deleteQuery").modal("show");
-      }
+      $.getJSON("${ url(app_name + ':delete_design') }", function(data) {
+        $("#deleteQueryMessage").text(data.title);
+      });
+      viewModel.chosenSavedQueries.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function(index) {
+        viewModel.chosenSavedQueries.push($(this).data("delete-name"));
+      });
+      $("#deleteQuery").modal("show");
     });
     });
 
 
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();

+ 24 - 14
apps/beeswax/src/beeswax/templates/show_tables.mako

@@ -72,7 +72,7 @@ ${layout.menubar(section='tables')}
                       <div class="hueCheckbox tableCheck"
                       <div class="hueCheckbox tableCheck"
                            data-view-url="${ url(app_name + ':describe_table', database=database, table=table) }"
                            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-browse-url="${ url(app_name + ':read_table', database=database, table=table) }"
-                           data-drop-url="${ url(app_name + ':drop_table', database=database, table=table) }" data-row-selector-exclude="true"></div>
+                           data-drop-name="${ table }" data-row-selector-exclude="true"></div>
                     </td>
                     </td>
                     <td>
                     <td>
                       <a href="${ url(app_name + ':describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>
                       <a href="${ url(app_name + ':describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>
@@ -85,8 +85,6 @@ ${layout.menubar(section='tables')}
     </div>
     </div>
 </div>
 </div>
 
 
-
-
 % if not examples_installed:
 % if not examples_installed:
 <div id="installSamples" class="modal hide fade">
 <div id="installSamples" class="modal hide fade">
   <div class="modal-header">
   <div class="modal-header">
@@ -106,7 +104,7 @@ ${layout.menubar(section='tables')}
 % endif
 % endif
 
 
 <div id="dropTable" class="modal hide fade">
 <div id="dropTable" class="modal hide fade">
-  <form id="dropTableForm" action="" method="POST">
+  <form id="dropTableForm" action="${ url(app_name + ':drop_table', database=database) }" method="POST">
     <div class="modal-header">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="dropTableMessage">${_('Confirm action')}</h3>
       <h3 id="dropTableMessage">${_('Confirm action')}</h3>
@@ -115,13 +113,24 @@ ${layout.menubar(section='tables')}
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
       <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
       <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
     </div>
+    <div class="hide">
+      <select name="table_selection" data-bind="options: availableTables, selectedOptions: chosenTables" size="5" multiple="true"></select>
+    </div>
   </form>
   </form>
 </div>
 </div>
 
 
 <script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
 <script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 
 
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function () {
   $(document).ready(function () {
+    var viewModel = {
+        availableTables : ko.observableArray(${ tables_json | n }),
+        chosenTables : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
     var tables = $(".datatables").dataTable({
     var tables = $(".datatables").dataTable({
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "bPaginate":false,
       "bPaginate":false,
@@ -208,20 +217,21 @@ ${layout.menubar(section='tables')}
             location.href = selector.data("browse-url")
             location.href = selector.data("browse-url")
           });
           });
         }
         }
-        if (selector.data("drop-url")) {
-          $("#dropBtn").removeAttr("disabled").data("confirmation-url", selector.data("drop-url"));
-        }
+      }
+      if (selector.length >= 1) {
+        $("#dropBtn").removeAttr("disabled");
       }
       }
     }
     }
 
 
     $("#dropBtn").click(function () {
     $("#dropBtn").click(function () {
-      if ($(this).data("confirmation-url")) {
-        $.getJSON($(this).data("confirmation-url"), function (data) {
-          $("#dropTableForm").attr("action", data.url);
-          $("#dropTableMessage").text(data.title);
-        });
-        $("#dropTable").modal("show");
-      }
+      $.getJSON("${ url(app_name + ':drop_table', database=database) }", function(data) {
+        $("#dropTableMessage").text(data.title);
+      });
+      viewModel.chosenTables.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenTables.push($(this).data("drop-name"));
+      });
+      $("#dropTable").modal("show");
     });
     });
   });
   });
 </script>
 </script>

+ 34 - 4
apps/beeswax/src/beeswax/tests.py

@@ -469,6 +469,22 @@ for x in sys.stdin:
     except:
     except:
       LOG.exception("Saw exception in child thread.")
       LOG.exception("Saw exception in child thread.")
 
 
+  def test_drop_multi_tables(self):
+    hql = """
+      CREATE TABLE test_drop_1 (a int);
+      CREATE TABLE test_drop_2 (a int);
+      CREATE TABLE test_drop_3 (a int);
+    """
+    resp = _make_query(self.client, hql)
+    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
+
+    # Drop them
+    resp = self.client.get('/beeswax/tables/drop/default')
+    assert_true('want to delete' in resp.content, resp.content)
+    resp = self.client.post('/beeswax/tables/drop/default', {u'table_selection': [u'test_drop_1', u'test_drop_2', u'test_drop_3']})
+    assert_equal(resp.status_code, 302)
+
+
   def test_multiple_statements_no_result_set(self):
   def test_multiple_statements_no_result_set(self):
     hql = """
     hql = """
       CREATE TABLE test_multiple_statements_1 (a int);
       CREATE TABLE test_multiple_statements_1 (a int);
@@ -503,7 +519,7 @@ for x in sys.stdin:
   def test_multiple_statements_various_queries(self):
   def test_multiple_statements_various_queries(self):
     hql = """
     hql = """
       CREATE TABLE test_multiple_statements_2 (a int);
       CREATE TABLE test_multiple_statements_2 (a int);
-      DROP TABLE test_multiple_statements_1;
+      DROP TABLE test_multiple_statements_2;
       SELECT foo FROM test;
       SELECT foo FROM test;
     """
     """
 
 
@@ -634,10 +650,24 @@ for x in sys.stdin:
     len_after = len(beeswax.models.SavedQuery.objects.filter(name__contains='rubbish'))
     len_after = len(beeswax.models.SavedQuery.objects.filter(name__contains='rubbish'))
     assert_true(len_after == len_before + 1)
     assert_true(len_after == len_before + 1)
 
 
+    # Make 3 more designs
+    resp = cli.get('/beeswax/clone_design/%s' % (design.id,))
+    resp = cli.get('/beeswax/clone_design/%s' % (design.id,))
+    resp = cli.get('/beeswax/clone_design/%s' % (design.id,))
+    designs = beeswax.models.SavedQuery.objects.filter(name__contains='rubbish')[:3]
+    print designs
+
     # Delete a design
     # Delete a design
-    resp = cli.get('/beeswax/delete_design/1')
-    assert_true('Delete design?' in resp.content)
-    resp = cli.post('/beeswax/delete_design/1')
+    resp = cli.get('/beeswax/delete_designs')
+    assert_true('Delete design(s)' in resp.content, resp.content)
+    resp = cli.post('/beeswax/delete_designs', {u'designs_selection': [u'1']})
+    assert_equal(resp.status_code, 302)
+
+    # Delete designs
+    design_ids = map(str, designs.values_list('id', flat=True))
+    resp = cli.get('/beeswax/delete_designs', {u'designs_selection': design_ids})
+    assert_true('Delete design(s)' in resp.content, resp.content)
+    resp = cli.post('/beeswax/delete_designs', {u'designs_selection': design_ids})
     assert_equal(resp.status_code, 302)
     assert_equal(resp.status_code, 302)
 
 
     # Helper to test the view, filtering, etc
     # Helper to test the view, filtering, etc

+ 2 - 2
apps/beeswax/src/beeswax/urls.py

@@ -21,11 +21,11 @@ urlpatterns = patterns('beeswax.views',
   url(r'^$', 'index', name='index'),
   url(r'^$', 'index', name='index'),
 
 
   url(r'^tables/(?P<database>\w+)?$', 'show_tables', name='show_tables'),
   url(r'^tables/(?P<database>\w+)?$', 'show_tables', name='show_tables'),
+  url(r'^tables/drop/(?P<database>\w+)$', 'drop_table', name='drop_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)$', 'describe_table', name='describe_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)$', 'describe_table', name='describe_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/load$', 'load_table', name='load_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/load$', 'load_table', name='load_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/read$', 'read_table', name='read_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/read$', 'read_table', name='read_table'),
-  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/drop$', 'drop_table', name='drop_table'),
 
 
   url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query'),
   url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query'),
   url(r'^explain_parameterized/(?P<design_id>\d+)$', 'explain_parameterized_query', name='explain_parameterized_query'),
   url(r'^explain_parameterized/(?P<design_id>\d+)$', 'explain_parameterized_query', name='explain_parameterized_query'),
@@ -38,7 +38,7 @@ urlpatterns = patterns('beeswax.views',
 
 
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),
-  url(r'^delete_design/(?P<design_id>\d+)$', 'delete_design', name='delete_design'),
+  url(r'^delete_designs$', 'delete_design', name='delete_design'),
   url(r'^clone_design/(?P<design_id>\d+)$', 'clone_design', name='clone_design'),
   url(r'^clone_design/(?P<design_id>\d+)$', 'clone_design', name='clone_design'),
   url(r'^query_history$', 'list_query_history', name='list_query_history'),
   url(r'^query_history$', 'list_query_history', name='list_query_history'),
 
 

+ 24 - 20
apps/beeswax/src/beeswax/views.py

@@ -131,19 +131,19 @@ def save_design(request, form, type, design, explicit_save):
 
 
 
 
 
 
-def delete_design(request, design_id):
-  """Delete a saved design"""
-  design = authorized_get_design(request, design_id)
-
-  if design is None:
-    LOG.error('Cannot delete non-existent design %s' % (design_id,))
-    return list_designs(request)
-
+def delete_design(request):
   if request.method == 'POST':
   if request.method == 'POST':
-    design.delete()
+    ids = request.POST.getlist('designs_selection')
+    designs = dict([(design_id, authorized_get_design(request, design_id)) for design_id in ids])
+
+    if None in designs.values():
+      LOG.error('Cannot delete non-existent design(s) %s' % ','.join([key for key, name in designs.items() if name is None]))
+      return list_designs(request)
+    for design in designs.values():
+      design.delete()
     return redirect(reverse(get_app_name(request) + ':list_designs'))
     return redirect(reverse(get_app_name(request) + ':list_designs'))
   else:
   else:
-    return render('confirm.html', request, dict(url=request.path, title=_('Delete design?')))
+    return render('confirm.html', request, dict(url=request.path, title=_('Delete design(s)?')))
 
 
 
 
 def clone_design(request, design_id):
 def clone_design(request, design_id):
@@ -201,6 +201,7 @@ def list_designs(request):
     'page': page,
     'page': page,
     'filter_params': filter_params,
     'filter_params': filter_params,
     'user': request.user,
     'user': request.user,
+    'designs_json': json.dumps([query.id for query in page.object_list])
   })
   })
 
 
 
 
@@ -242,6 +243,7 @@ def my_queries(request):
     'h_page': hist_page,
     'h_page': hist_page,
     'q_page': query_page,
     'q_page': query_page,
     'filter_params': filter_params,
     'filter_params': filter_params,
+    'designs_json': json.dumps([query.id for query in query_page.object_list])
   })
   })
 
 
 
 
@@ -301,12 +303,14 @@ def show_tables(request, database=None):
 
 
   tables = db.get_tables(database=database)
   tables = db.get_tables(database=database)
   examples_installed = beeswax.models.MetaInstall.get().installed_example
   examples_installed = beeswax.models.MetaInstall.get().installed_example
+  #table_selection = TableSelection(tables=tables)
 
 
   return render("show_tables.mako", request, {
   return render("show_tables.mako", request, {
       'tables': tables,
       'tables': tables,
       'examples_installed': examples_installed,
       'examples_installed': examples_installed,
       'db_form': db_form,
       'db_form': db_form,
       'database': database,
       'database': database,
+      'tables_json': json.dumps(tables),
   })
   })
 
 
 
 
@@ -333,25 +337,25 @@ def describe_table(request, database, table):
   })
   })
 
 
 
 
-def drop_table(request, database, table):
+def drop_table(request, database):
   db = dbms.get(request.user)
   db = dbms.get(request.user)
 
 
-  table = db.get_table(database, table)
-
   if request.method == 'POST':
   if request.method == 'POST':
+    tables = request.POST.getlist('table_selection')
+    tables_objects = [db.get_table(database, table) for table in tables]
+    app_name = get_app_name(request)
     try:
     try:
-      query_history = db.drop_table(database, table)
-      url = reverse(get_app_name(request) + ':watch_query', args=[query_history.id]) + '?on_success_url=' + reverse(get_app_name(request) + ':show_tables')
+      # Can't be simpler without an important refactoring
+      design = SavedQuery.create_empty(app_name=app_name, owner=request.user)
+      query_history = db.drop_tables(database, tables_objects, design)
+      url = reverse(app_name + ':watch_query', args=[query_history.id]) + '?on_success_url=' + reverse(app_name + ':show_tables')
       return redirect(url)
       return redirect(url)
     except BeeswaxException, ex:
     except BeeswaxException, ex:
       error_message, log = expand_exception(ex, db)
       error_message, log = expand_exception(ex, db)
-      error = _("Failed to remove %(table)s.  Error: %(error)s") % {'table': table.name, 'error': error_message}
+      error = _("Failed to remove %(tables)s.  Error: %(error)s") % {'tables': ','.join(tables), 'error': error_message}
       raise PopupException(error, title=_("Beeswax Error"), detail=log)
       raise PopupException(error, title=_("Beeswax Error"), detail=log)
   else:
   else:
-    if table.is_view:
-      title = _("Do you really want to drop the view '%(table)s'?") % {'table': table.name}
-    else:
-      title = _("This may delete the underlying data as well as the metadata. Drop table '%(table)s'?") % {'table': table.name}
+    title = _("Do you really want to delete the table(s)?")
     return render('confirm.html', request, dict(url=request.path, title=title))
     return render('confirm.html', request, dict(url=request.path, title=title))