Jelajahi Sumber

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 tahun lalu
induk
melakukan
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 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 cli_service.ttypes import TSessionHandle, THandleIdentifier,\
   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_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
 
   class Meta:
@@ -279,6 +279,17 @@ class SavedQuery(models.Model):
     design.is_auto = copy.deepcopy(self.is_auto)
     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
   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)
 
 
+  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):
     """Beeswax does not support use directly."""
     if SERVER_INTERFACE.get() == HIVE_SERVER2:
@@ -174,6 +189,7 @@ class Dbms:
       curr = time.time()
     return None
 
+
   def execute_next_statement(self, query_history):
     query_history.statement_number += 1
     query_history.last_state = QueryHistory.STATE.submitted.index
@@ -181,6 +197,7 @@ class Dbms:
     query = query_history.design.get_design()
     return self.execute_and_watch(query, query_history=query_history)
 
+
   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.

+ 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">
-    <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">
         <a href="#" class="close" data-dismiss="modal">&times;</a>
         <h3>${_('Drop Table')}</h3>
@@ -144,6 +144,11 @@ ${layout.menubar(section='tables')}
         <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
         <input type="submit" class="btn btn-danger" value="${_('Yes, drop this table')}"/>
     </div>
+    <div class="hide">
+      <select name="table_selection">
+        <option value="${ table.name }" selected>${ table.name }</option>
+      </select>
+    </div>
     </form>
 </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);
      });
 

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

@@ -19,6 +19,7 @@
     from desktop.views import commonheader, commonfooter
     from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
@@ -67,7 +68,7 @@ ${layout.menubar(section='saved queries')}
           <div class="hueCheckbox savedCheck"
             % if may_edit:
               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}"
             % endif
             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 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">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <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="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
+    <div class="hide">
+      <select name="designs_selection" data-bind="options: availableSavedQueries, selectedOptions: chosenSavedQueries" multiple="true"></select>
+    </div>
   </form>
 </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">
   $(document).ready(function () {
+    var viewModel = {
+        availableSavedQueries : ko.observableArray(${ designs_json | n }),
+        chosenSavedQueries : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
     var savedQueries = $(".datatables").dataTable({
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "bPaginate":false,
@@ -179,20 +192,21 @@ ${layout.menubar(section='saved queries')}
             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 () {
-      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();

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

@@ -17,6 +17,7 @@
 import time
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
+from beeswax import models
 from beeswax.views import collapse_whitespace
 %>
 
@@ -100,9 +101,6 @@ ${ layout.menubar(section='history') }
               </tr>
             </thead>
             <tbody>
-            <%!
-              from beeswax import models, views
-            %>
             % for query in page.object_list:
               <%
                 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 desktop.views import commonheader, commonfooter
 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="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
-<%!  from beeswax.views import collapse_whitespace %>
 
 ${ commonheader(_('My Queries'), app_name, user, '100px') | n,unicode }
 ${layout.menubar(section='my queries')}
@@ -68,14 +70,15 @@ ${layout.menubar(section='my queries')}
           </tr>
         </thead>
         <tbody>
-        <%!
-          from beeswax import models
-        %>
         % for design in q_page.object_list:
           <tr>
             <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>
               <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>
         </thead>
         <tbody>
-        <%!
-          from beeswax import models, views
-        %>
         % 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>
             <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:
                   data-view-url="${ url(app_name + ':watch_query', id=query.id) }?context=${qcontext|u}"
                 % endif
@@ -152,7 +148,7 @@ ${layout.menubar(section='my queries')}
 </div>
 
 <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">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <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="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
+    <div class="hide">
+      <select name="designs_selection" data-bind="options: availableSavedQueries, selectedOptions: chosenSavedQueries" multiple="true"></select>
+    </div>
   </form>
 </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">
   $(document).ready(function () {
+    var viewModel = {
+        availableSavedQueries : ko.observableArray(${ designs_json | n }),
+        chosenSavedQueries : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
 
     updateQueryCounters();
 
@@ -175,7 +182,7 @@ ${layout.menubar(section='my queries')}
       "bLengthChange":false,
       "bInfo":false,
       "aaSorting":[
-        [4, 'desc']
+        [1, 'asc']
       ],
       "aoColumns":[
         {"bSortable":false, "sWidth":"1%" },
@@ -272,9 +279,10 @@ ${layout.menubar(section='my queries')}
             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 () {
-      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();

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

@@ -72,7 +72,7 @@ ${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-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>
                       <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>
 
-
-
 % if not examples_installed:
 <div id="installSamples" class="modal hide fade">
   <div class="modal-header">
@@ -106,7 +104,7 @@ ${layout.menubar(section='tables')}
 % endif
 
 <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">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <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="submit" class="btn btn-danger" value="${_('Yes')}"/>
     </div>
+    <div class="hide">
+      <select name="table_selection" data-bind="options: availableTables, selectedOptions: chosenTables" size="5" multiple="true"></select>
+    </div>
   </form>
 </div>
 
 <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">
   $(document).ready(function () {
+    var viewModel = {
+        availableTables : ko.observableArray(${ tables_json | n }),
+        chosenTables : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
     var tables = $(".datatables").dataTable({
       "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
       "bPaginate":false,
@@ -208,20 +217,21 @@ ${layout.menubar(section='tables')}
             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 () {
-      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>

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

@@ -469,6 +469,22 @@ for x in sys.stdin:
     except:
       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):
     hql = """
       CREATE TABLE test_multiple_statements_1 (a int);
@@ -503,7 +519,7 @@ for x in sys.stdin:
   def test_multiple_statements_various_queries(self):
     hql = """
       CREATE TABLE test_multiple_statements_2 (a int);
-      DROP TABLE test_multiple_statements_1;
+      DROP TABLE test_multiple_statements_2;
       SELECT foo FROM test;
     """
 
@@ -634,10 +650,24 @@ for x in sys.stdin:
     len_after = len(beeswax.models.SavedQuery.objects.filter(name__contains='rubbish'))
     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
-    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)
 
     # 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'^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+)/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+)/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'^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'^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'^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':
-    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'))
   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):
@@ -201,6 +201,7 @@ def list_designs(request):
     'page': page,
     'filter_params': filter_params,
     '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,
     'q_page': query_page,
     '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)
   examples_installed = beeswax.models.MetaInstall.get().installed_example
+  #table_selection = TableSelection(tables=tables)
 
   return render("show_tables.mako", request, {
       'tables': tables,
       'examples_installed': examples_installed,
       'db_form': db_form,
       '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)
 
-  table = db.get_table(database, table)
-
   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:
-      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)
     except BeeswaxException, ex:
       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)
   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))