Эх сурвалжийг харах

HUE-1187 [beeswax] Trash for queries

Queries can be bulk deleted and restored.
Added tests.
Clean up a bit Oozie trashes
Romain Rigaux 12 жил өмнө
parent
commit
587a7c3

+ 109 - 0
apps/beeswax/src/beeswax/migrations/0007_auto__add_field_savedquery_is_trashed.py

@@ -0,0 +1,109 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Adding field 'SavedQuery.is_trashed'
+        db.add_column('beeswax_savedquery', 'is_trashed', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True, blank=True), keep_default=False)
+
+
+    def backwards(self, orm):
+
+        # Deleting field 'SavedQuery.is_trashed'
+        db.delete_column('beeswax_savedquery', 'is_trashed')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'beeswax.metainstall': {
+            'Meta': {'object_name': 'MetaInstall'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'installed_example': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+        },
+        'beeswax.queryhistory': {
+            'Meta': {'object_name': 'QueryHistory'},
+            'design': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['beeswax.SavedQuery']", 'null': 'True'}),
+            'has_results': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_state': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'log_context': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'modified_row_count': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'notify': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'operation_type': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'query': ('django.db.models.fields.TextField', [], {}),
+            'server_guid': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '1024', 'null': 'True'}),
+            'server_host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_id': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'server_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_port': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'server_type': ('django.db.models.fields.CharField', [], {'default': "'beeswax'", 'max_length': '128'}),
+            'statement_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'beeswax.savedquery': {
+            'Meta': {'object_name': 'SavedQuery'},
+            'data': ('django.db.models.fields.TextField', [], {'max_length': '65536'}),
+            'desc': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_auto': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'is_trashed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'mtime': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'beeswax.session': {
+            'Meta': {'object_name': 'Session'},
+            'application': ('django.db.models.fields.CharField', [], {'default': "'beeswax'", 'max_length': '128'}),
+            'guid': ('django.db.models.fields.TextField', [], {'max_length': "'100'"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_used': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'secret': ('django.db.models.fields.TextField', [], {'max_length': "'100'"}),
+            'server_protocol_version': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'status_code': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['beeswax']

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

@@ -267,6 +267,8 @@ class SavedQuery(models.Model):
   # An auto design is a place-holder for things users submit but not saved.
   # We still want to store it as a design to allow users to save them later.
   is_auto = models.BooleanField(default=False, db_index=True)
+  is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),
+                                   help_text=_t('If this query is trashed.'))
 
   class Meta:
     ordering = ['-mtime']

+ 31 - 10
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -36,11 +36,22 @@ ${layout.menubar(section='saved queries')}
       <button id="editBtn" class="btn toolbarBtn" title="${_('Edit the selected query')}" disabled="disabled"><i class="icon-edit"></i> ${_('Edit')}</button>
       <button id="cloneBtn" class="btn toolbarBtn" title="${_('Clone the selected query')}" disabled="disabled"><i class="icon-retweet"></i> ${_('Clone')}</button>
       <button id="historyBtn" class="btn toolbarBtn" title="${_('View the usage history of the selected query')}" disabled="disabled"><i class="icon-tasks"></i> ${_('Usage history')}</button>
-      <button id="deleteBtn" class="btn toolbarBtn" title="${_('Delete the selected queries')}" disabled="disabled"><i class="icon-trash"></i>  ${_('Delete')}</button>
+
+      <div id="delete-dropdown" class="btn-group" style="display: inline">
+        <button href="#" class="btn dropdown-toggle" title="${_('Delete selected queries')}" data-toggle="dropdown" disabled="disabled">
+          <i class="icon-remove"></i> ${_('Delete')}
+          <span class="caret"></span>
+        </button>
+        <ul class="dropdown-menu" style="top: auto">
+          <li><a href="#" id="trashQueryBtn" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
+          <li><a href="#" id="deleteQueryBtn" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
+        </ul>
+      </div>
     </%def>
 
     <%def name="creation()">
-      <a class="btn" href="${ url(app_name + ':execute_query') }"><i class="icon-plus-sign"></i> ${_('Create New Query')}</a>
+      <a class="btn" href="${ url(app_name + ':execute_query') }" title="${_('New query')}"><i class="icon-plus-sign"></i> ${_('Create New Query')}</a>
+      <a class="btn" href="${ url(app_name + ':list_trashed_designs') }" title="${_('Go to the trash')}"><i class="icon-trash"></i> ${_('Trash')}</a>
     </%def>
   </%actionbar:render>
 
@@ -55,9 +66,6 @@ ${layout.menubar(section='saved queries')}
       </tr>
     </thead>
     <tbody>
-      <%!
-        from beeswax import models
-      %>
       % for design in page.object_list:
         <%
           may_edit = user == design.owner
@@ -95,6 +103,7 @@ ${layout.menubar(section='saved queries')}
 
 <div id="deleteQuery" class="modal hide fade">
   <form id="deleteQueryForm" action="${ url(app_name + ':delete_design') }" method="POST">
+    <input type="hidden" name="skipTrash" id="skipTrash" value="false"/>
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
@@ -171,6 +180,8 @@ ${layout.menubar(section='saved queries')}
 
     function toggleActions() {
       $(".toolbarBtn").attr("disabled", "disabled");
+      $(".btn.dropdown-toggle").attr("disabled", "disabled");
+
       var selector = $(".hueCheckbox[checked='checked']");
       if (selector.length == 1) {
         if (selector.data("edit-url")) {
@@ -190,19 +201,29 @@ ${layout.menubar(section='saved queries')}
         }
       }
       if (selector.length >= 1) {
-        $("#deleteBtn").removeAttr("disabled");
+        $(".btn.dropdown-toggle").removeAttr("disabled");
       }
     }
 
-    $("#deleteBtn").click(function () {
-      $.getJSON("${ url(app_name + ':delete_design') }", function(data) {
-        $("#deleteQueryMessage").text(data.title);
-      });
+    function deleteQueries() {
       viewModel.chosenSavedQueries.removeAll();
       $(".hueCheckbox[checked='checked']").each(function( index ) {
         viewModel.chosenSavedQueries.push($(this).data("delete-name"));
       });
+
       $("#deleteQuery").modal("show");
+    }
+
+    $("#trashQueryBtn").click(function () {
+      $("#skipTrash").val(false);
+      $("#deleteQueryMessage").text("${ _('Move the selected queries to the trash?') }");
+      deleteQueries();
+    });
+
+    $("#deleteQueryBtn").click(function () {
+      $("#skipTrash").val(true);
+      $("#deleteQueryMessage").text("${ _('Delete the selected queries?') }");
+      deleteQueries();
     });
 
     $("a[data-row-selector='true']").jHueRowSelector();

+ 216 - 0
apps/beeswax/src/beeswax/templates/list_trashed_designs.mako

@@ -0,0 +1,216 @@
+## 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.
+<%!
+    import time
+    from django.template.defaultfilters import timesince
+    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" />
+
+${ commonheader(_('Saved Queries'), app_name, user, '100px') | n,unicode }
+
+${layout.menubar(section='saved queries')}
+
+<div class="container-fluid">
+  <h1>${_('Trashed Queries')}</h1>
+
+  <%actionbar:render>
+    <%def name="actions()">
+      <button id="deleteQueryBtn" class="btn toolbarBtn" title="${_('Delete forever')}" disabled="disabled">
+        <i class="icon-bolt"></i> ${_('Delete forever')}
+      </button>
+      <button id="restoreQueryBtn" class="btn toolbarBtn" title="${_('Restore from trash')}" disabled="disabled">
+        <i class="icon-cloud-upload"></i> ${_('Restore')}
+      </button>
+    </%def>
+
+    <%def name="creation()">
+      <button id="emptyTrashBtn" class="btn" title="${_('Empty trash')}" data-bind="enabled: availableSavedQueries().length > 0">
+        <i class="icon-fire"></i> ${_('Empty')}
+      </button>
+    </%def>
+  </%actionbar:render>
+
+  <table class="table table-striped table-condensed datatables">
+    <thead>
+      <tr>
+        <th width="1%"><div class="hueCheckbox selectAll" data-selectables="savedCheck"></div></th>
+        <th>${_('Name')}</th>
+        <th>${_('Description')}</th>
+        <th>${_('Owner')}</th>
+        <th>${_('Last Modified')}</th>
+      </tr>
+    </thead>
+    <tbody>
+      % for design in page.object_list:
+        <%
+          may_edit = user == design.owner
+        %>
+      <tr>
+        <td data-row-selector-exclude="true">
+          <div class="hueCheckbox savedCheck"
+            % if may_edit:
+              data-delete-id="${ design.id }"
+            % endif
+            data-row-selector-exclude="true">
+         </div>
+        </td>
+        <td>
+         ${ design.name }
+        </td>
+        <td>
+        % if design.desc:
+          ${ design.desc }
+        % endif
+        </td>
+        <td>${ design.owner.username }</td>
+        <td data-sort-value="${time.mktime(design.mtime.timetuple())}">${ timesince(design.mtime) } ${_('ago')}</td>
+      </tr>
+      % endfor
+    </tbody>
+  </table>
+  ${comps.pagination(page)}
+</div>
+
+<div id="deleteQuery" class="modal hide fade">
+  <form id="deleteQueryForm" action="${ url(app_name + ':delete_design') }" method="POST">
+    <input type="hidden" name="skipTrash" id="skipTrash" value="true"/>
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
+    </div>
+    <div class="modal-footer">
+      <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,
+      "bLengthChange":false,
+      "bInfo":false,
+      "aaSorting":[
+        [4, 'desc']
+      ],
+      "aoColumns":[
+        {"bSortable":false, "sWidth":"1%" },
+        null,
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" }
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sZeroRecords":"${_('No matching records')}",
+      },
+      "bStateSave": true
+    });
+
+    $("#filterInput").keyup(function () {
+      savedQueries.fnFilter($(this).val());
+    });
+
+    $(".selectAll").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeAttr("checked").removeClass("icon-ok");
+        $("." + $(this).data("selectables")).removeClass("icon-ok").removeAttr("checked");
+      }
+      else {
+        $(this).attr("checked", "checked").addClass("icon-ok");
+        $("." + $(this).data("selectables")).addClass("icon-ok").attr("checked", "checked");
+      }
+      toggleActions();
+    });
+
+    $(".savedCheck").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeClass("icon-ok").removeAttr("checked");
+      }
+      else {
+        $(this).addClass("icon-ok").attr("checked", "checked");
+      }
+      $(".selectAll").removeAttr("checked").removeClass("icon-ok");
+      toggleActions();
+    });
+
+    function toggleActions() {
+      $(".toolbarBtn").attr("disabled", "disabled");
+
+      var selector = $(".hueCheckbox[checked='checked']");
+
+      if (selector.length >= 1) {
+        $(".toolbarBtn").removeAttr("disabled");
+      }
+    }
+
+    function restoreQuery() {
+      viewModel.chosenSavedQueries.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenSavedQueries.push($(this).data("delete-id"));
+      });
+
+      $("#deleteQuery").modal("show");
+    }
+
+    $("#restoreQueryBtn").click(function () {
+      $("#deleteQueryForm").attr("action", "${ url(app_name + ':restore_design') }");
+      $("#deleteQueryMessage").text("${ _('Restore the selected queries?') }");
+      restoreQuery();
+    });
+
+    $("#deleteQueryBtn").click(function () {
+      $("#deleteQueryForm").attr("action", "${ url(app_name + ':delete_design') }");
+      $("#deleteQueryMessage").text("${ _('Delete the selected queries?') }");
+      restoreQuery();
+    });
+
+    $("#emptyTrashBtn").click(function () {
+      $("#deleteQueryForm").attr("action", "${ url(app_name + ':delete_design') }");
+      $("#deleteQueryMessage").text("${ _('Empty the trash?') }");
+
+      viewModel.chosenSavedQueries.removeAll();
+      $.each(viewModel.availableSavedQueries(), function(index, query) {
+        viewModel.chosenSavedQueries.push(query);
+      });
+
+      $("#deleteQuery").modal("show");
+    });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 31 - 8
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -44,11 +44,21 @@ ${layout.menubar(section='my queries')}
       <button id="editBtn" class="btn toolbarBtn" title="${_('Edit the selected query')}" disabled="disabled"><i class="icon-edit"></i> ${_('Edit')}</button>
       <button id="cloneBtn" class="btn toolbarBtn" title="${_('Clone the selected query')}" disabled="disabled"><i class="icon-retweet"></i> ${_('Clone')}</button>
       <button id="historyBtn" class="btn toolbarBtn" title="${_('View the usage history of the selected query')}" disabled="disabled"><i class="icon-tasks"></i> ${_('Usage history')}</button>
-      <button id="deleteBtn" class="btn toolbarBtn" title="${_('Delete the selected queries')}" disabled="disabled"><i class="icon-trash"></i>  ${_('Delete')}</button>
+      <div id="delete-dropdown" class="btn-group" style="display: inline">
+        <button href="#" class="btn dropdown-toggle" title="${_('Delete selected queries')}" data-toggle="dropdown" disabled="disabled">
+          <i class="icon-remove"></i> ${_('Delete')}
+          <span class="caret"></span>
+        </button>
+        <ul class="dropdown-menu" style="top: auto">
+          <li><a href="#" id="trashQueryBtn" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
+          <li><a href="#" id="deleteQueryBtn" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
+        </ul>
+      </div>
     </%def>
 
     <%def name="creation()">
-      <a class="btn" href="${ url(app_name + ':execute_query') }"><i class="icon-plus-sign"></i> ${_('Create New Query')}</a>
+      <a class="btn" href="${ url(app_name + ':execute_query') }" title="${_('New query')}"><i class="icon-plus-sign"></i> ${_('Create New Query')}</a>
+      <a class="btn" href="${ url(app_name + ':list_trashed_designs') }" title="${_('Go to the trash')}"><i class="icon-trash"></i> ${_('Trash')}</a>
     </%def>
   </%actionbar:render>
 
@@ -145,6 +155,7 @@ ${layout.menubar(section='my queries')}
 
 <div id="deleteQuery" class="modal hide fade">
   <form id="deleteQueryForm" action="${ url(app_name + ':delete_design') }" method="POST">
+    <input type="hidden" name="skipTrash" id="skipTrash" value="false"/>
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="deleteQueryMessage">${_('Confirm action')}</h3>
@@ -254,6 +265,8 @@ ${layout.menubar(section='my queries')}
 
     function toggleActions() {
       $(".toolbarBtn").attr("disabled", "disabled");
+      $(".btn.dropdown-toggle").attr("disabled", "disabled");
+
       var selector = $(".hueCheckbox[checked='checked']");
       if (selector.length == 1) {
         if (selector.data("view-url")) {
@@ -279,7 +292,7 @@ ${layout.menubar(section='my queries')}
       }
 
       if (selector.length >= 1 && $('#recentSavedQueries').hasClass('active')) {
-        $("#deleteBtn").removeAttr("disabled");
+        $(".btn.dropdown-toggle").removeAttr("disabled");
       }
     }
 
@@ -294,15 +307,25 @@ ${layout.menubar(section='my queries')}
       }
     }
 
-    $("#deleteBtn").click(function () {
-      $.getJSON("${ url(app_name + ':delete_design') }", function(data) {
-        $("#deleteQueryMessage").text(data.title);
-      });
+    function deleteQueries() {
       viewModel.chosenSavedQueries.removeAll();
-      $(".hueCheckbox[checked='checked']").each(function(index) {
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
         viewModel.chosenSavedQueries.push($(this).data("delete-name"));
       });
+
       $("#deleteQuery").modal("show");
+    }
+
+    $("#trashQueryBtn").click(function () {
+      $("#skipTrash").val(false);
+      $("#deleteQueryMessage").text("${ _('Move the selected queries to the trash?') }");
+      deleteQueries();
+    });
+
+    $("#deleteQueryBtn").click(function () {
+      $("#skipTrash").val(true);
+      $("#deleteQueryMessage").text("${ _('Delete the selected queries?') }");
+      deleteQueries();
     });
 
     $("a[data-row-selector='true']").jHueRowSelector();

+ 46 - 1
apps/beeswax/src/beeswax/tests.py

@@ -1485,7 +1485,7 @@ class MockDbms:
     return ['default', 'test']
 
 
-class MockServer(object):
+class TestWithMockedServer(object):
 
   def setUp(self):
     # Beware: Monkey patch Beeswax/Hive server with Mock API
@@ -1530,6 +1530,51 @@ class MockServer(object):
     finally:
       design.delete()
 
+  def test_bulk_query_trash(self):
+    response = _make_query(self.client, 'SELECT', submission_type='Save', name='My Name 1', desc='My Description')
+    query = response.context['design']
+    response = _make_query(self.client, 'SELECT', submission_type='Save', name='My Name 2', desc='My Description')
+    query2 = response.context['design']
+    ids = [query.id, query2.id]
+
+    resp = self.client.get('/beeswax/list_designs')
+    ids_page_1 = set([query.id for query in resp.context['page'].object_list])
+    assert_equal(2, sum([query_id in ids_page_1 for query_id in ids]))
+
+    resp = self.client.post(reverse('beeswax:delete_design'), {u'skipTrash': [u'false'], u'designs_selection': ids})
+    queries = SavedQuery.objects.filter(id__in=ids)
+    assert_true(queries[0].is_trashed)
+    assert_true(queries[1].is_trashed)
+
+    resp = self.client.get('/beeswax/list_designs')
+    ids_page_1 = set([query.id for query in resp.context['page'].object_list])
+    assert_equal(0, sum([query_id in ids_page_1 for query_id in ids]))
+
+    resp = self.client.post(reverse('beeswax:restore_design'), {u'skipTrash': [u'false'], u'designs_selection': ids})
+    query = SavedQuery.objects.filter(id__in=ids)
+    assert_false(queries[0].is_trashed)
+    assert_false(queries[1].is_trashed)
+
+    resp = self.client.get('/beeswax/list_designs')
+    ids_page_1 = set([query.id for query in resp.context['page'].object_list])
+    assert_equal(2, sum([query_id in ids_page_1 for query_id in ids]))
+
+    resp = self.client.post(reverse('beeswax:delete_design'), {u'skipTrash': [u'false'], u'designs_selection': ids})
+    query = SavedQuery.objects.filter(id__in=ids)
+    assert_true(queries[0].is_trashed)
+    assert_true(queries[1].is_trashed)
+
+    resp = self.client.get('/beeswax/list_designs')
+    ids_page_1 = set([query.id for query in resp.context['page'].object_list])
+    assert_equal(0, sum([query_id in ids_page_1 for query_id in ids]))
+
+    resp = self.client.post(reverse('beeswax:delete_design'), {u'skipTrash': [u'true'], u'designs_selection': ids})
+    assert_false(SavedQuery.objects.filter(id__in=ids).exists())
+
+    resp = self.client.get('/beeswax/list_designs')
+    ids_page_1 = set([query.id for query in resp.context['page'].object_list])
+    assert_equal(0, sum([query_id in ids_page_1 for query_id in ids]))
+
 
 def search_log_line(component, expected_log, all_logs):
   """Checks if 'expected_log' can be found in one line of 'all_logs' outputed by the logging component 'component'."""

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

@@ -32,7 +32,9 @@ urlpatterns = patterns('beeswax.views',
 
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),
+  url(r'^list_trashed_designs$', 'list_trashed_designs', name='list_trashed_designs'),
   url(r'^delete_designs$', 'delete_design', name='delete_design'),
+  url(r'^restore_designs$', 'restore_design', name='restore_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'),
 

+ 48 - 3
apps/beeswax/src/beeswax/views.py

@@ -152,13 +152,35 @@ def delete_design(request):
     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()
+      if request.POST.get('skipTrash', 'false') == 'false':
+        design.is_trashed = True
+        design.save()
+      else:
+        design.delete()
     return redirect(reverse(get_app_name(request) + ':list_designs'))
   else:
     return render('confirm.html', request, dict(url=request.path, title=_('Delete design(s)?')))
 
 
+def restore_design(request):
+  if request.method == 'POST':
+    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 restore 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.is_trashed = False
+      design.save()
+    return redirect(reverse(get_app_name(request) + ':list_designs'))
+  else:
+    return render('confirm.html', request, dict(url=request.path, title=_('Restore design(s)?')))
+
+
 def clone_design(request, design_id):
   """Clone a design belonging to any user"""
   design = authorized_get_design(request, design_id)
@@ -218,6 +240,29 @@ def list_designs(request):
   })
 
 
+def list_trashed_designs(request):
+  DEFAULT_PAGE_SIZE = 20
+  app_name= get_app_name(request)
+
+  user = request.user
+
+  # Extract the saved query list.
+  prefix = 'q-'
+  querydict_query = _copy_prefix(prefix, request.GET)
+  # Manually limit up the user filter.
+  querydict_query[ prefix + 'user' ] = user
+  querydict_query[ prefix + 'type' ] = app_name
+  page, filter_params = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix, is_trashed=True)
+
+  return render('list_trashed_designs.mako', request, {
+    'page': page,
+    'filter_params': filter_params,
+    'user': request.user,
+    'designs_json': json.dumps([query.id for query in page.object_list])
+  })
+
+
+
 def my_queries(request):
   """
   View a mix of history and saved queries.
@@ -1107,7 +1152,7 @@ def execute_directly(request, query, query_server=None, design=None, tablename=N
   return format_preserving_redirect(request, watch_url, get_dict)
 
 
-def _list_designs(querydict, page_size, prefix=""):
+def _list_designs(querydict, page_size, prefix="", is_trashed=False):
   """
   _list_designs(querydict, page_size, prefix, user) -> (page, filter_param)
 
@@ -1126,7 +1171,7 @@ def _list_designs(querydict, page_size, prefix=""):
   )
 
   # Filtering. Only display designs explicitly saved.
-  db_queryset = models.SavedQuery.objects.filter(is_auto=False)
+  db_queryset = models.SavedQuery.objects.filter(is_auto=False, is_trashed=is_trashed)
 
   user = querydict.get(prefix + 'user')
   if user is not None:

+ 1 - 0
apps/filebrowser/src/filebrowser/templates/listdir.mako

@@ -27,6 +27,7 @@ ${ commonheader(_('File Browser'), 'filebrowser', user) | n,unicode }
 
 <div class="container-fluid">
     <h1>${_('File Browser')}</h1>
+
     <div class="actionbar">
     <%actionbar:render>
         <%def name="search()">

+ 3 - 3
apps/oozie/src/oozie/templates/editor/list_bundles.mako

@@ -36,13 +36,13 @@ ${ layout.menubar(section='bundles') }
         <button class="btn toolbarBtn" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
         <button class="btn toolbarBtn" id="clone-btn" disabled="disabled"><i class="icon-retweet"></i> ${ _('Clone') }</button>
         <div id="delete-dropdown" class="btn-group" style="display: inline">
-          <button id="delete-btn" class="btn delete-link dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
+          <button id="delete-btn" class="btn toolbarBtn dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
             <i class="icon-remove"></i> ${_('Delete')}
             <span class="caret"></span>
           </button>
           <ul class="dropdown-menu" style="top: auto">
-            <li><a href="javascript:void(0);" id="trash-btn" class="delete-link" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
-            <li><a href="javascript:void(0);" id="destroy-btn" class="delete-link" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
+            <li><a href="javascript:void(0);" id="trash-btn" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
+            <li><a href="javascript:void(0);" id="destroy-btn" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
           </ul>
         </div>
     </%def>

+ 4 - 4
apps/oozie/src/oozie/templates/editor/list_coordinators.mako

@@ -28,20 +28,20 @@ ${ layout.menubar(section='coordinators') }
 
 
 <div class="container-fluid">
-  <h1>${ _('Coordinator Trash') }</h1>
+  <h1>${ _('Coordinator Manager') }</h1>
 
   <%actionbar:render>
     <%def name="actions()">
         <button class="btn toolbarBtn" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
         <button class="btn toolbarBtn" id="clone-btn" disabled="disabled"><i class="icon-retweet"></i> ${ _('Clone') }</button>
         <div id="delete-dropdown" class="btn-group" style="display: inline">
-          <button id="delete-btn" class="btn delete-link dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
+          <button id="delete-btn" class="btn toolbarBtn dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
             <i class="icon-remove"></i> ${_('Delete')}
             <span class="caret"></span>
           </button>
           <ul class="dropdown-menu" style="top: auto">
-            <li><a href="javascript:void(0);" id="trash-btn" class="delete-link" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
-            <li><a href="javascript:void(0);" id="destroy-btn" class="delete-link" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
+            <li><a href="javascript:void(0);" id="trash-btn" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
+            <li><a href="javascript:void(0);" id="destroy-btn" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
           </ul>
         </div>
     </%def>

+ 7 - 5
apps/oozie/src/oozie/templates/editor/list_trashed_bundles.mako

@@ -33,14 +33,16 @@ ${ layout.menubar(section='bundles') }
 
   <%actionbar:render>
     <%def name="actions()">
-      <a href="${ url('oozie:list_bundles') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Bundles') }</a>
+      <a href="${ url('oozie:list_bundles') }" id="home-btn" class="btn" title="${ _('Got to bundle manager') }"><i class="icon-home"></i> ${ _('Bundles') }</a>
+      &nbsp;&nbsp;
+      <button class="btn toolbarBtn" id="restore-btn" disabled="disabled" title="${ _('Retore the selected bundles') }"><i class="icon-cloud-upload"></i> ${ _('Restore') }</button>
+      <button class="btn toolbarBtn" id="destroy-btn" disabled="disabled" title="${ _('Delete the selected bundles') }"><i class="icon-bolt"></i> ${ _('Delete forever') }</button>
     </%def>
 
     <%def name="creation()">
-      <button class="btn toolbarBtn" id="restore-btn" disabled="disabled"><i class="icon-plus"></i> ${ _('Restore') }</button>
-      <button class="btn toolbarBtn" id="destroy-btn" disabled="disabled"><i class="icon-bolt"></i> ${ _('Delete forever') }</button>
-      &nbsp;&nbsp;
-      <button class="btn" id="purge-btn"><i class="icon-fire"></i> ${ _('Empty') }</button>
+      <button class="btn" id="purge-btn" title="${ _('Delete all the bundles') }" data-bind="enabled: availableJobs().length > 0">
+        <i class="icon-fire"></i> ${ _('Empty') }
+      </button>
     </%def>
   </%actionbar:render>
 

+ 10 - 4
apps/oozie/src/oozie/templates/editor/list_trashed_coordinators.mako

@@ -32,13 +32,19 @@ ${ layout.menubar(section='coordinators') }
 
   <%actionbar:render>
     <%def name="actions()">
-        <a href="${ url('oozie:list_coordinators') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Coordinators') }</a>
+        <a href="${ url('oozie:list_coordinators') }" id="home-btn" class="btn" title="${ _('Got to coordinator manager') }">
+          <i class="icon-home"></i> ${ _('Coordinators') }
+        </a>
+        &nbsp;&nbsp;
+        <button class="btn toolbarBtn" id="restore-btn" disabled="disabled" title="${_('Restore the selected coordinators')}">
+          <i class="icon-cloud-upload"></i> ${ _('Restore') }
+        </button>
+        <button class="btn toolbarBtn" id="destroy-btn" disabled="disabled" title="${_('Delete the selected coordinators')}">
+          <i class="icon-bolt"></i> ${ _('Delete forever') }
+        </button>
     </%def>
 
     <%def name="creation()">
-        <button class="btn toolbarBtn" id="restore-btn" disabled="disabled"><i class="icon-plus"></i> ${ _('Restore') }</button>
-        <button class="btn toolbarBtn" id="destroy-btn" disabled="disabled"><i class="icon-bolt"></i> ${ _('Delete forever') }</button>
-        &nbsp;&nbsp;
         <button class="btn" id="purge-btn"><i class="icon-fire"></i> ${ _('Empty') }</button>
     </%def>
   </%actionbar:render>

+ 13 - 5
apps/oozie/src/oozie/templates/editor/list_trashed_workflows.mako

@@ -32,14 +32,22 @@ ${ layout.menubar(section='workflows') }
 
   <%actionbar:render>
     <%def name="actions()">
-      <a href="${ url('oozie:list_workflows') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Workflows') }</a>
+      <a href="${ url('oozie:list_workflows') }" id="home-btn" class="btn" title="${ _('Got to workflow manager') }">
+        <i class="icon-home"></i> ${ _('Workflows') }
+      </a>
+      &nbsp;&nbsp;
+      <button type="button" id="restore-btn" class="btn" title="${ _('Restore the selected workflows') }">
+        <i class="icon-cloud-upload"></i> ${ _('Restore') }
+      </button>
+      <button type="button" id="destroy-btn" class="btn" title="${ _('Delete the selected workflows') }">
+        <i class="icon-bolt"></i> ${ _('Delete forever') }
+      </button>
     </%def>
 
     <%def name="creation()">
-      <button type="button" id="restore-btn" class="btn"><i class="icon-plus-sign"></i> ${ _('Restore') }</button>
-      <button type="button" id="destroy-btn" class="btn"><i class="icon-bolt"></i> ${ _('Delete forever') }</button>
-      &nbsp;&nbsp;
-      <button type="button" id="purge-btn" class="btn"><i class="icon-fire"></i> ${ _('Purge trash') }</button>
+      <button type="button" id="purge-btn" class="btn" title="${ _('Restore all the workflows') }">
+        <i class="icon-fire"></i> ${ _('Empty') }
+      </button>
     </%def>
   </%actionbar:render>
 

+ 3 - 3
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -36,13 +36,13 @@ ${ layout.menubar(section='workflows') }
       <button class="btn toolbarBtn" id="schedule-btn" disabled="disabled"><i class="icon-calendar"></i> ${ _('Schedule') }</button>
       <button class="btn toolbarBtn" id="clone-btn" disabled="disabled"><i class="icon-retweet"></i> ${ _('Clone') }</button>
       <div id="delete-dropdown" class="btn-group" style="display: inline">
-        <button id="delete-btn" class="btn delete-link dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
+        <button id="delete-btn" class="btn toolbarBtn dropdown-toggle" title="${_('Delete')}" data-toggle="dropdown" disabled="disabled">
           <i class="icon-remove"></i> ${_('Delete')}
           <span class="caret"></span>
         </button>
         <ul class="dropdown-menu" style="top: auto">
-          <li><a href="javascript:void(0);" id="trash-btn" class="delete-link" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
-          <li><a href="javascript:void(0);" id="destroy-btn" class="delete-link" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
+          <li><a href="javascript:void(0);" id="trash-btn" title="${_('Move to Trash')}"><i class="icon-trash"></i> ${_('Move to Trash')}</a></li>
+          <li><a href="javascript:void(0);" id="destroy-btn" title="${_('Delete forever')}"><i class="icon-bolt"></i> ${_('Delete forever')}</a></li>
         </ul>
       </div>
     </%def>

+ 44 - 41
apps/pig/src/pig/api.py

@@ -59,47 +59,50 @@ class OozieApi:
     }
 
     workflow = Workflow.objects.new_workflow(self.user)
-    workflow.name = OozieApi.WORKFLOW_NAME
-    workflow.is_history = True
-    workflow.save()
-    Workflow.objects.initialize(workflow, self.fs)
-
-    script_path = workflow.deployment_dir + '/script.pig'
-    self.fs.create(script_path, data=pig_script.dict['script'])
-
-    pig_params = []
-    for param in json.loads(params):
-      pig_params.append({"type":"argument","value":"-param"})
-      pig_params.append({"type":"argument","value":"%(name)s=%(value)s" % param})
-
-    files = []
-    archives = []
-
-    for resource in pig_script.dict['resources']:
-      if resource['type'] == 'file':
-        files.append(resource['value'])
-      if resource['type'] == 'archive':
-        archives.append({"dummy": "", "name": resource['value']})
-
-    action = Pig.objects.create(
-        name='pig',
-        script_path=script_path,
-        workflow=workflow,
-        node_type='pig',
-        params=json.dumps(pig_params),
-        files=json.dumps(files),
-        archives=json.dumps(archives),
-    )
-
-    action.add_node(workflow.end)
-
-    start_link = workflow.start.get_link()
-    start_link.child = action
-    start_link.save()
-
-    oozie_wf = _submit_workflow(self.user, self.fs, workflow, mapping)
-
-    workflow.delete()
+
+    try:
+      workflow.name = OozieApi.WORKFLOW_NAME
+      workflow.is_history = True
+      workflow.save()
+      Workflow.objects.initialize(workflow, self.fs)
+
+      script_path = workflow.deployment_dir + '/script.pig'
+      self.fs.create(script_path, data=pig_script.dict['script'])
+
+      pig_params = []
+      for param in json.loads(params):
+        pig_params.append({"type":"argument","value":"-param"})
+        pig_params.append({"type":"argument","value":"%(name)s=%(value)s" % param})
+
+      files = []
+      archives = []
+
+      for resource in pig_script.dict['resources']:
+        if resource['type'] == 'file':
+          files.append(resource['value'])
+        if resource['type'] == 'archive':
+          archives.append({"dummy": "", "name": resource['value']})
+
+      action = Pig.objects.create(
+          name='pig',
+          script_path=script_path,
+          workflow=workflow,
+          node_type='pig',
+          params=json.dumps(pig_params),
+          files=json.dumps(files),
+          archives=json.dumps(archives),
+      )
+
+      action.add_node(workflow.end)
+
+      start_link = workflow.start.get_link()
+      start_link.child = action
+      start_link.save()
+
+      oozie_wf = _submit_workflow(self.user, self.fs, workflow, mapping)
+    finally:
+      workflow.delete()
+
     return oozie_wf
 
   def stop(self, job_id):

+ 1 - 1
apps/pig/src/pig/templates/app.mako

@@ -130,7 +130,7 @@ ${ commonheader(_('Pig'), "pig", user, "100px") | n,unicode }
             </li>
             <li data-bind="click: showStopModal, visible: currentScript().isRunning()">
               <a href="#" title="${ _('Run the script') }" rel="tooltip" data-placement="right" class="disabled">
-                <i class="icon-spinner icon-ban-circle"></i> ${ _('Stop') }
+                <i class="icon-spinner icon-spin"></i><i class="icon-ban-circle"></i> ${ _('Stop') }
               </a>
             </li>
             <li data-bind="visible: currentScript().id() != -1, click: copyScript">