Преглед на файлове

HUE-1186 [oozie] Trash for designs

Update workflows, coordinators, and bundles.
Added 2 tests for workflows.
Modified 3 tests for permissions (one for each job type).
Abraham Elmahrek преди 12 години
родител
ревизия
d17cb15

+ 326 - 0
apps/oozie/src/oozie/migrations/0021_auto__chg_field_java_args__add_field_job_is_trashed.py

@@ -0,0 +1,326 @@
+# 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):
+        
+        # Changing field 'Java.args'
+        db.alter_column('oozie_java', 'args', self.gf('django.db.models.fields.TextField')(blank=True))
+
+        # Adding field 'Job.is_trashed'
+        db.add_column('oozie_job', 'is_trashed', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True, blank=True), keep_default=False)
+    
+    
+    def backwards(self, orm):
+        
+        # Changing field 'Java.args'
+        db.alter_column('oozie_java', 'args', self.gf('django.db.models.fields.CharField')(max_length=4096, blank=True))
+
+        # Deleting field 'Job.is_trashed'
+        db.delete_column('oozie_job', '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'})
+        },
+        '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'})
+        },
+        'oozie.bundle': {
+            'Meta': {'object_name': 'Bundle', '_ormbases': ['oozie.Job']},
+            'coordinators': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['oozie.Coordinator']", 'through': "orm['oozie.BundledCoordinator']", 'symmetrical': 'False'}),
+            'job_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Job']", 'unique': 'True', 'primary_key': 'True'}),
+            'kick_off_time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 4, 17, 18, 21, 34, 305797)'})
+        },
+        'oozie.bundledcoordinator': {
+            'Meta': {'object_name': 'BundledCoordinator'},
+            'bundle': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Bundle']"}),
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parameters': ('django.db.models.fields.TextField', [], {'default': '\'[{"name":"oozie.use.system.libpath","value":"true"}]\''})
+        },
+        'oozie.coordinator': {
+            'Meta': {'object_name': 'Coordinator', '_ormbases': ['oozie.Job']},
+            'concurrency': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'end': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 4, 20, 18, 21, 34, 302627)'}),
+            'execution': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'frequency_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+            'frequency_unit': ('django.db.models.fields.CharField', [], {'default': "'days'", 'max_length': '20'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Job']", 'unique': 'True', 'primary_key': 'True'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 4, 17, 18, 21, 34, 302576)'}),
+            'throttle': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'timeout': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'America/Los_Angeles'", 'max_length': '24'}),
+            'workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']", 'null': 'True'})
+        },
+        'oozie.datainput': {
+            'Meta': {'object_name': 'DataInput'},
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'dataset': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Dataset']", 'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'})
+        },
+        'oozie.dataoutput': {
+            'Meta': {'object_name': 'DataOutput'},
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'dataset': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Dataset']", 'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'})
+        },
+        'oozie.dataset': {
+            'Meta': {'object_name': 'Dataset'},
+            'advanced_end_instance': ('django.db.models.fields.CharField', [], {'default': "'0'", 'max_length': '128', 'blank': 'True'}),
+            'advanced_start_instance': ('django.db.models.fields.CharField', [], {'default': "'0'", 'max_length': '128'}),
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'done_flag': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
+            'frequency_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+            'frequency_unit': ('django.db.models.fields.CharField', [], {'default': "'days'", 'max_length': '20'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'instance_choice': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '10'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 4, 17, 18, 21, 34, 303603)'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'America/Los_Angeles'", 'max_length': '24'}),
+            'uri': ('django.db.models.fields.CharField', [], {'default': "'/data/${YEAR}${MONTH}${DAY}'", 'max_length': '1024'})
+        },
+        'oozie.decision': {
+            'Meta': {'object_name': 'Decision'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.decisionend': {
+            'Meta': {'object_name': 'DecisionEnd'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.distcp': {
+            'Meta': {'object_name': 'DistCp'},
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.email': {
+            'Meta': {'object_name': 'Email'},
+            'body': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'cc': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'subject': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'to': ('django.db.models.fields.TextField', [], {'default': "''"})
+        },
+        'oozie.end': {
+            'Meta': {'object_name': 'End'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.fork': {
+            'Meta': {'object_name': 'Fork'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.fs': {
+            'Meta': {'object_name': 'Fs'},
+            'chmods': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'deletes': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'mkdirs': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'moves': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'touchzs': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'})
+        },
+        'oozie.generic': {
+            'Meta': {'object_name': 'Generic'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'xml': ('django.db.models.fields.TextField', [], {'default': "''"})
+        },
+        'oozie.history': {
+            'Meta': {'object_name': 'History'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Job']"}),
+            'oozie_job_id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'properties': ('django.db.models.fields.TextField', [], {}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'oozie.hive': {
+            'Meta': {'object_name': 'Hive'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': '\'[{"name":"oozie.hive.defaults","value":"hive-site.xml"}]\''}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        'oozie.java': {
+            'Meta': {'object_name': 'Java'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'args': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'capture_output': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'jar_path': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'java_opts': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'main_class': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.job': {
+            'Meta': {'object_name': 'Job'},
+            'deployment_dir': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_shared': ('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'}),
+            'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'parameters': ('django.db.models.fields.TextField', [], {'default': '\'[{"name":"oozie.use.system.libpath","value":"true"}]\''}),
+            'schema_version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'oozie.join': {
+            'Meta': {'object_name': 'Join'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.kill': {
+            'Meta': {'object_name': 'Kill'},
+            'message': ('django.db.models.fields.CharField', [], {'default': "'Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]'", 'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.link': {
+            'Meta': {'object_name': 'Link'},
+            'child': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parent_node'", 'to': "orm['oozie.Node']"}),
+            'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'child_node'", 'to': "orm['oozie.Node']"})
+        },
+        'oozie.mapreduce': {
+            'Meta': {'object_name': 'Mapreduce'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'jar_path': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True'}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.node': {
+            'Meta': {'object_name': 'Node'},
+            'children': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'parents'", 'symmetrical': 'False', 'through': "orm['oozie.Link']", 'to': "orm['oozie.Node']"}),
+            'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'node_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']"})
+        },
+        'oozie.pig': {
+            'Meta': {'object_name': 'Pig'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        'oozie.shell': {
+            'Meta': {'object_name': 'Shell'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'capture_output': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'command': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.sqoop': {
+            'Meta': {'object_name': 'Sqoop'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
+        },
+        'oozie.ssh': {
+            'Meta': {'object_name': 'Ssh'},
+            'capture_output': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'command': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'host': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'user': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        },
+        'oozie.start': {
+            'Meta': {'object_name': 'Start'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True'})
+        },
+        'oozie.streaming': {
+            'Meta': {'object_name': 'Streaming'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'mapper': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'reducer': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        'oozie.subworkflow': {
+            'Meta': {'object_name': 'SubWorkflow'},
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'propagate_configuration': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'sub_workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']"})
+        },
+        'oozie.workflow': {
+            'Meta': {'object_name': 'Workflow', '_ormbases': ['oozie.Job']},
+            'end': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'end_workflow'", 'null': 'True', 'to': "orm['oozie.End']"}),
+            'is_single': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Job']", 'unique': 'True', 'primary_key': 'True'}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'start': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'start_workflow'", 'null': 'True', 'to': "orm['oozie.Start']"})
+        }
+    }
+    
+    complete_apps = ['oozie']

+ 29 - 2
apps/oozie/src/oozie/models.py

@@ -57,6 +57,14 @@ name_validator = RegexValidator(regex='[a-zA-Z_][\-_a-zA-Z0-9]{1,39}',
                                 message=_('Enter a valid value: combination of 2 - 40 letters and digits starting by a letter'))
 
 
+class TrashManager(models.Manager):
+  def trashed(self):
+    return super(TrashManager, self).get_query_set().filter(is_trashed=True)
+
+  def get_query_set(self):
+    return super(TrashManager, self).get_query_set().filter(is_trashed=False)
+
+
 """
 Permissions:
 
@@ -113,10 +121,25 @@ class Job(models.Model):
                                   help_text=_t('Enable other users to have access to this job.'))
   parameters = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', verbose_name=_t('Oozie parameters'),
                                 help_text=_t('Parameters used at the submission time (e.g. market=US, oozie.use.system.libpath=true).'))
+  is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),
+                                   help_text=_t('If this job is trashed.'))
 
   objects = JobManager()
   unique_together = ('owner', 'name')
 
+  def delete(self, skip_trash=False, *args, **kwargs):
+    if skip_trash:
+      return super(Job, self).delete(*args, **kwargs)
+    else:
+      self.is_trashed = True
+      self.save()
+      return self
+
+  def restore(self):
+    self.is_trashed = False
+    self.save()
+    return self
+
   def save(self):
     super(Job, self).save()
 
@@ -184,7 +207,7 @@ class Job(models.Model):
     return user.is_superuser or self.owner == user
 
 
-class WorkflowManager(models.Manager):
+class WorkflowManager(TrashManager):
   def new_workflow(self, owner):
     workflow = Workflow(owner=owner, schema_version='uri:oozie:workflow:0.4')
 
@@ -233,7 +256,7 @@ class WorkflowManager(models.Manager):
     except:
       pass
     workflow.save()
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
 class Workflow(Job):
@@ -1228,6 +1251,8 @@ class Coordinator(Job):
   job_properties = models.TextField(default='[]', verbose_name=_t('Workflow properties'),
                                     help_text=_t('Configuration properties to transmit to the workflow (e.g. limit=100, username=${coord:user()})'))
 
+  objects = TrashManager()
+
   HUE_ID = 'hue-id-c'
 
   def get_type(self):
@@ -1480,6 +1505,8 @@ class Bundle(Job):
                                        help_text=_t('When to start the first coordinators.'))
   coordinators = models.ManyToManyField(Coordinator, through='BundledCoordinator')
 
+  objects = TrashManager()
+
   HUE_ID = 'hue-id-b'
 
   def get_type(self):

+ 41 - 14
apps/oozie/src/oozie/templates/editor/list_bundles.mako

@@ -35,11 +35,22 @@ ${ layout.menubar(section='bundles') }
     <%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>
-        <button class="btn toolbarBtn" id="delete-btn" disabled="disabled"><i class="icon-remove"></i> ${ _('Delete') }</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">
+            <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>
+          </ul>
+        </div>
     </%def>
 
     <%def name="creation()">
         <a href="${ url('oozie:create_bundle') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+        &nbsp;&nbsp;
+        <a href="${ url('oozie:list_trashed_bundles') }" class="btn"><i class="icon-trash"></i> ${ _('Trash') }</a>
     </%def>
   </%actionbar:render>
 
@@ -99,11 +110,27 @@ ${ layout.menubar(section='bundles') }
 
 <div id="submit-job-modal" class="modal hide"></div>
 
-<div id="delete-job" class="modal hide">
-  <form id="deleteWfForm" action="${ url('oozie:delete_bundle') }" method="POST">
+<div id="trash-job" class="modal hide">
+  <form id="trashForm" action="${ url('oozie:delete_bundle') }" method="POST">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
-      <h3 id="deleteWfMessage">${ _('Delete the selected bundle(s)?') }</h3>
+      <h3 id="trashMessage">${ _('Move the selected bundle(s) to trash?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="destroy-job" class="modal hide">
+  <form id="destroyForm" action="${ url('oozie:delete_bundle') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="destroyMessage">${ _('Delete the selected bundle(s)?') }</h3>
     </div>
     <div class="modal-footer">
       <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
@@ -173,12 +200,20 @@ ${ layout.menubar(section='bundles') }
       }
     }
 
-    $("#delete-btn").click(function (e) {
+    $("#trash-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("delete-id"));
+      });
+      $("#trash-job").modal("show");
+    });
+
+    $("#destroy-btn").click(function (e) {
       viewModel.chosenJobs.removeAll();
       $(".hueCheckbox[checked='checked']").each(function( index ) {
         viewModel.chosenJobs.push($(this).data("delete-id"));
       });
-      $("#delete-job").modal("show");
+      $("#destroy-job").modal("show");
     });
 
     $("#submit-btn").click(function () {
@@ -191,14 +226,6 @@ ${ layout.menubar(section='bundles') }
       );
     });
 
-    $(".deleteConfirmation").click(function () {
-      var _this = $(this);
-      var _action = _this.attr("data-url");
-      $("#deleteWfForm").attr("action", _action);
-      $("#deleteWfMessage").text(_this.attr("alt"));
-      $("#delete-job").modal("show");
-    });
-
     $("#clone-btn").click(function (e) {
       var _this = $(".hueCheckbox[checked='checked']");
       var _url = _this.attr("data-clone-url");

+ 42 - 7
apps/oozie/src/oozie/templates/editor/list_coordinators.mako

@@ -28,17 +28,28 @@ ${ layout.menubar(section='coordinators') }
 
 
 <div class="container-fluid">
-  <h1>${ _('Coordinator Manager') }</h1>
+  <h1>${ _('Coordinator Trash') }</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>
-        <button class="btn toolbarBtn" id="delete-btn" disabled="disabled"><i class="icon-remove"></i> ${ _('Delete') }</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">
+            <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>
+          </ul>
+        </div>
     </%def>
 
     <%def name="creation()">
         <a href="${ url('oozie:create_coordinator') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+        &nbsp;&nbsp;
+        <a href="${ url('oozie:list_trashed_coordinators') }" class="btn"><i class="icon-trash"></i> ${ _('Trash') }</a>
     </%def>
   </%actionbar:render>
 
@@ -95,11 +106,27 @@ ${ layout.menubar(section='coordinators') }
 
 <div id="submit-job-modal" class="modal hide"></div>
 
-<div id="delete-job" class="modal hide">
-  <form id="deleteWfForm" action="${ url('oozie:delete_coordinator') }" method="POST">
+<div id="trash-job" class="modal hide">
+  <form id="trashForm" action="${ url('oozie:delete_coordinator') }" method="POST">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
-      <h3 id="deleteWfMessage">${ _('Delete the selected coordinator(s)?') }</h3>
+      <h3 id="trashMessage">${ _('Move the selected coordinator(s) to trash?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="destroy-job" class="modal hide">
+  <form id="destroyForm" action="${ url('oozie:delete_coordinator') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="destroyMessage">${ _('Delete the selected coordinator(s)?') }</h3>
     </div>
     <div class="modal-footer">
       <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
@@ -169,12 +196,20 @@ ${ layout.menubar(section='coordinators') }
       }
     }
 
-    $("#delete-btn").click(function (e) {
+    $("#trash-btn").click(function (e) {
       viewModel.chosenJobs.removeAll();
       $(".hueCheckbox[checked='checked']").each(function( index ) {
         viewModel.chosenJobs.push($(this).data("delete-id"));
       });
-      $("#delete-job").modal("show");
+      $("#trash-job").modal("show");
+    });
+
+    $("#destroy-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("delete-id"));
+      });
+      $("#destroy-job").modal("show");
     });
 
     $("#submit-btn").click(function () {

+ 254 - 0
apps/oozie/src/oozie/templates/editor/list_trashed_bundles.mako

@@ -0,0 +1,254 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+  import time as py_time
+%>
+
+<%namespace name="actionbar" file="../actionbar.mako" />
+<%namespace name="layout" file="../navigation-bar.mako" />
+<%namespace name="utils" file="../utils.inc.mako" />
+
+${ commonheader(_("Oozie App"), "oozie", user, "100px") | n,unicode }
+${ layout.menubar(section='bundles') }
+
+
+<div class="container-fluid">
+  <h1>${ _('Bundle Trash') }</h1>
+
+  <%actionbar:render>
+    <%def name="actions()">
+      <a href="${ url('oozie:list_bundles') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Bundles') }</a>
+    </%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>
+
+  <table id="bundleTable" class="table datatables">
+    <thead>
+      <tr>
+        <th width="1%"><div class="hueCheckbox selectAll" data-selectables="bundleCheck"></div></th>
+        <th width="10%">${ _('Name') }</th>
+        <th width="20%">${ _('Description') }</th>
+        <th width="35%">${ _('Coordinators') }</th>
+        <th>${ _('Kick off') }</th>
+        <th>${ _('Status') }</th>
+        <th>${ _('Last Modified') }</th>
+        <th>${ _('Owner') }</th>
+      </tr>
+    </thead>
+    <tbody>
+      % for bundle in jobs:
+        <tr>
+          <td data-row-selector-exclude="true">
+            <div class="hueCheckbox bundleCheck" data-row-selector-exclude="true" data-bundle-id="${ bundle.id }"></div>
+          </td>
+          <td>${ bundle.name }</td>
+          <td>${ bundle.description }</td>
+          <td>
+             % for bundled in bundle.coordinators.all():
+               ${ bundled.coordinator.name }
+           % if not loop.last:
+            ,
+           % endif
+             % endfor
+          </td>
+          <td>${ bundle.kick_off_time }</td>
+          <td>
+            <span class="label label-info">${ bundle.status }</span>
+          </td>
+          <td nowrap="nowrap" data-sort-value="${py_time.mktime(bundle.last_modified.timetuple())}">${ utils.format_date(bundle.last_modified) }</td>
+          <td>${ bundle.owner.username }</td>
+        </tr>
+      %endfor
+    </tbody>
+  </table>
+</div>
+
+
+<div id="purge-job" class="modal hide">
+  <form id="purgeForm" action="${ url('oozie:delete_bundle') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="purgefMessage">${ _('Delete all bundle(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="destroy-job" class="modal hide">
+  <form id="destroyForm" action="${ url('oozie:delete_bundle') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="destroyMessage">${ _('Delete the selected bundle(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="restore-job" class="modal hide">
+  <form id="restoreForm" action="${ url('oozie:restore_bundle') }" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="restoreMessage">${ _('Restore the selected bundle(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></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 = {
+        availableJobs : ko.observableArray(${ json_jobs | n }),
+        chosenJobs : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
+    $(".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();
+    });
+
+    $(".bundleCheck").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']");
+      var can_modify = $(".hueCheckbox[checked='checked'][data-bundle-id]");
+      if (can_modify.length >= 1 && can_modify.length == selector.length) {
+        $("#destroy-btn").removeAttr("disabled");
+        $("#restore-btn").removeAttr("disabled");
+      }
+    }
+
+    $("#purge-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("bundle-id"));
+      });
+      $("#purge-job").modal("show");
+    });
+
+    $("#destroy-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("bundle-id"));
+      });
+      $("#destroy-job").modal("show");
+    });
+
+    $("#restore-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("bundle-id"));
+      });
+      $("#restore-job").modal("show");
+    });
+
+    var oTable = $("#bundleTable").dataTable({
+      "sPaginationType":"bootstrap",
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
+      "aoColumns":[
+        { "bSortable":false },
+        null,
+        null,
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
+        null,
+        null
+      ],
+      "aaSorting":[
+        [ 6, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
+        }
+      }
+    });
+
+    $("#filterInput").keydown(function (e) {
+      if (e.which == 13) {
+        e.preventDefault();
+        return false;
+      }
+    });
+
+    $("#filterInput").keyup(function () {
+      oTable.fnFilter($(this).val());
+    });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 252 - 0
apps/oozie/src/oozie/templates/editor/list_trashed_coordinators.mako

@@ -0,0 +1,252 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+  import time as py_time
+%>
+<%namespace name="actionbar" file="../actionbar.mako" />
+<%namespace name="layout" file="../navigation-bar.mako" />
+<%namespace name="utils" file="../utils.inc.mako" />
+
+${ commonheader(_("Oozie App"), "oozie", user, "100px") | n,unicode }
+${ layout.menubar(section='coordinators') }
+
+
+<div class="container-fluid">
+  <h1>${ _('Coordinator Trash') }</h1>
+
+  <%actionbar:render>
+    <%def name="actions()">
+        <a href="${ url('oozie:list_coordinators') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Coordinators') }</a>
+    </%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>
+
+  <table id="coordinatorTable" class="table datatables">
+    <thead>
+      <tr>
+        <th width="1%"><div class="hueCheckbox selectAll" data-selectables="coordinatorCheck"></div></th>
+        <th>${ _('Name') }</th>
+        <th>${ _('Description') }</th>
+        <th>${ _('Workflow') }</th>
+        <th>${ _('Frequency') }</th>
+        <th>${ _('Status') }</th>
+        <th>${ _('Last Modified') }</th>
+        <th>${ _('Owner') }</th>
+      </tr>
+    </thead>
+    <tbody>
+      %for coordinator in jobs:
+        <tr>
+          <td data-row-selector-exclude="true">
+            <div class="hueCheckbox coordinatorCheck" data-row-selector-exclude="true" data-coordinator-id="${ coordinator.id }"></div>
+          </td>
+          <td>${ coordinator.name }</td>
+          <td>${ coordinator.description }</td>
+          <td>
+            % if coordinator.workflow is not None:
+              ${ coordinator.workflow }
+            % endif
+          </td>
+          <td>${ coordinator.text_frequency }</td>
+          <td>
+            <span class="label label-info">${ coordinator.status }</span>
+          </td>
+          <td nowrap="nowrap" data-sort-value="${py_time.mktime(coordinator.last_modified.timetuple())}">${ utils.format_date(coordinator.last_modified) }</td>
+          <td>${ coordinator.owner.username }</td>
+        </tr>
+      %endfor
+    </tbody>
+  </table>
+</div>
+
+
+<div id="purge-job" class="modal hide">
+  <form id="purgeForm" action="${ url('oozie:delete_coordinator') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="purgeMessage">${ _('Delete all coordinator(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+
+<div id="destroy-job" class="modal hide">
+  <form id="purgeForm" action="${ url('oozie:delete_coordinator') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="purgeMessage">${ _('Delete the selected coordinator(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+
+<div id="restore-job" class="modal hide">
+  <form id="restoreWfForm" action="${ url('oozie:restore_coordinator') }" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="restoreWfMessage">${ _('Restore the selected coordinator(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></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 = {
+        availableJobs : ko.observableArray(${ json_jobs | n }),
+        chosenJobs : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
+    $(".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();
+    });
+
+    $(".coordinatorCheck").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']");
+      var can_modify = $(".hueCheckbox[checked='checked'][data-coordinator-id]");
+      if (can_modify.length >= 1 && can_modify.length == selector.length) {
+        $("#destroy-btn").removeAttr("disabled");
+        $("#restore-btn").removeAttr("disabled");
+      }
+    }
+
+    $("#purge-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("coordinator-id"));
+      });
+      $("#purge-job").modal("show");
+    });
+
+    $("#destroy-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("coordinator-id"));
+      });
+      $("#destroy-job").modal("show");
+    });
+
+    $("#restore-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("coordinator-id"));
+      });
+      $("#restore-job").modal("show");
+    });
+
+    var oTable = $("#coordinatorTable").dataTable({
+      "sPaginationType":"bootstrap",
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
+      "aoColumns":[
+        { "bSortable":false },
+        null,
+        null,
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
+        null,
+        null
+      ],
+      "aaSorting":[
+        [ 6, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
+        }
+      }
+    });
+
+    $("#filterInput").keydown(function (e) {
+      if (e.which == 13) {
+        e.preventDefault();
+        return false;
+      }
+    });
+
+    $("#filterInput").keyup(function () {
+      oTable.fnFilter($(this).val());
+    });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 249 - 0
apps/oozie/src/oozie/templates/editor/list_trashed_workflows.mako

@@ -0,0 +1,249 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+  import time as py_time
+%>
+<%namespace name="actionbar" file="../actionbar.mako" />
+<%namespace name="layout" file="../navigation-bar.mako" />
+<%namespace name="utils" file="../utils.inc.mako" />
+
+${ commonheader(_("Oozie App"), "oozie", user, "100px") | n,unicode }
+${ layout.menubar(section='workflows') }
+
+
+<div class="container-fluid">
+  <h1>${ _('Workflow Trash') }</h1>
+
+  <%actionbar:render>
+    <%def name="actions()">
+      <a href="${ url('oozie:list_workflows') }" id="home-btn" class="btn"><i class="icon-home"></i> ${ _('Workflows') }</a>
+    </%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>
+    </%def>
+  </%actionbar:render>
+
+
+  <table id="workflowTable" class="table datatables">
+    <thead>
+      <tr>
+        <th width="1%"><div class="hueCheckbox selectAll" data-selectables="workflowCheck"></div></th>
+        <th>${ _('Name') }</th>
+        <th>${ _('Description') }</th>
+        <th>${ _('Last Modification') }</th>
+        <th>${ _('Steps') }</th>
+        <th>${ _('Status') }</th>
+        <th>${ _('Owner') }</th>
+      </tr>
+    </thead>
+    <tbody>
+      % for workflow in jobs:
+        <tr>
+          <td data-row-selector-exclude="true">
+             <div class="hueCheckbox workflowCheck" data-row-selector-exclude="true" data-workflow-id="${ workflow.id }"></div>
+          </td>
+          <td>
+            ${ workflow.name }
+          </td>
+          <td>${ workflow.description }</td>
+
+          <td nowrap="nowrap" data-sort-value="${py_time.mktime(workflow.last_modified.timetuple())}">${ utils.format_date(workflow.last_modified) }</td>
+          <td><span class="badge badge-info">${ workflow.actions.count() }</span></td>
+          <td>
+            <span class="label label-info">${ workflow.status }</span>
+          </td>
+          <td>${ workflow.owner.username }</td>
+        </tr>
+      % endfor
+    </tbody>
+  </table>
+
+</div>
+
+
+<div id="destroyWf" class="modal hide fade">
+  <form id="destroyWfForm" action="${ url('oozie:delete_workflow') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="destroyWfMessage">${ _('Delete the selected workflow(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="purgeWfs" class="modal hide fade">
+  <form id="purgeWfsForm" action="${ url('oozie:delete_workflow') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="purgeWfsMessage">${ _('Delete all trashed workflow(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="restoreWf" class="modal hide fade">
+  <form id="restoreWfForm" action="${ url('oozie:restore_workflow') }" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="restoreWfMessage">${ _('Restore the selected workflow(s)?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></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 = {
+        availableJobs : ko.observableArray(${ json_jobs | n }),
+        chosenJobs : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
+    $(".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();
+    });
+
+    $(".workflowCheck").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']");
+      var can_delete = $(".hueCheckbox[checked='checked'][data-workflow-id]");
+      if (can_delete.length >= 1 && can_delete.length == selector.length) {
+        $("#destroy-btn").removeAttr("disabled");
+      }
+    }
+
+    $("#destroy-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("workflow-id"));
+      });
+      $("#destroyWf").modal("show");
+    });
+
+    $("#purge-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("workflow-id"));
+      });
+      $("#purgeWfs").modal("show");
+    });
+
+    $("#restore-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("workflow-id"));
+      });
+      $("#restoreWf").modal("show");
+    });
+
+    var oTable = $("#workflowTable").dataTable({
+      "sPaginationType":"bootstrap",
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
+      "aoColumns":[
+        { "bSortable":false },
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
+        null,
+        null,
+        null
+      ],
+      "aaSorting":[
+        [3, 'desc'],
+        [ 1, 'asc' ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
+        }
+      }
+    });
+
+    $("#filterInput").keydown(function (e) {
+      if (e.which == 13) {
+        e.preventDefault();
+        return false;
+      }
+    });
+
+    $("#filterInput").keyup(function () {
+      oTable.fnFilter($(this).val());
+    });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
+</script>
+
+${commonfooter(messages) |n,unicode}

+ 46 - 11
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -32,15 +32,26 @@ ${ layout.menubar(section='workflows') }
 
   <%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="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>
-        <button class="btn toolbarBtn" id="delete-btn" disabled="disabled"><i class="icon-remove"></i> ${ _('Delete') }</button>
+      <button class="btn toolbarBtn" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
+      <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">
+          <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>
+        </ul>
+      </div>
     </%def>
 
     <%def name="creation()">
-        <a href="${ url('oozie:create_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
-        <a href="${ url('oozie:import_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Import') }</a>
+      <a href="${ url('oozie:create_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+      <a href="${ url('oozie:import_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Import') }</a>
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_trashed_workflows') }" class="btn"><i class="icon-trash"></i> ${ _('Trash') }</a>
     </%def>
   </%actionbar:render>
 
@@ -96,11 +107,27 @@ ${ layout.menubar(section='workflows') }
 
 <div id="submit-wf-modal" class="modal hide"></div>
 
-<div id="deleteWf" class="modal hide fade">
-  <form id="deleteWfForm" action="${ url('oozie:delete_workflow') }" method="POST">
+<div id="trashWf" class="modal hide fade">
+  <form id="trashWfForm" action="${ url('oozie:delete_workflow') }" method="POST">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
-      <h3 id="deleteWfMessage">${ _('Delete the selected workflow(s)?') }</h3>
+      <h3 id="trashWfMessage">${ _('Move the selected workflow(s) to trash?') }</h3>
+    </div>
+    <div class="modal-footer">
+      <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
+      <input type="submit" class="btn btn-danger" value="${ _('Yes') }"/>
+    </div>
+    <div class="hide">
+      <select name="job_selection" data-bind="options: availableJobs, selectedOptions: chosenJobs" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<div id="destroyWf" class="modal hide fade">
+  <form id="destroyWfForm" action="${ url('oozie:delete_workflow') }?skip_trash=true" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="destroyWfMessage">${ _('Delete the selected workflow(s)?') }</h3>
     </div>
     <div class="modal-footer">
       <a href="#" class="btn" data-dismiss="modal">${ _('No') }</a>
@@ -171,12 +198,20 @@ ${ layout.menubar(section='workflows') }
       }
     }
 
-    $("#delete-btn").click(function (e) {
+    $("#trash-btn").click(function (e) {
+      viewModel.chosenJobs.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenJobs.push($(this).data("delete-id"));
+      });
+      $("#trashWf").modal("show");
+    });
+
+    $("#destroy-btn").click(function (e) {
       viewModel.chosenJobs.removeAll();
       $(".hueCheckbox[checked='checked']").each(function( index ) {
         viewModel.chosenJobs.push($(this).data("delete-id"));
       });
-      $("#deleteWf").modal("show");
+      $("#destroyWf").modal("show");
     });
 
     $("#submit-btn").click(function () {

+ 111 - 20
apps/oozie/src/oozie/tests.py

@@ -934,7 +934,7 @@ class TestEditor(OozieMockBase):
             '</configuration>\W+'
         '</sub-workflow>', xml, re.MULTILINE), xml)
 
-    wf2.delete()
+    wf2.delete(skip_trash=True)
 
   def test_workflow_flatten_list(self):
     assert_equal('[<Start: start>, <Mapreduce: action-name-1>, <Mapreduce: action-name-2>, <Mapreduce: action-name-3>, '
@@ -1292,6 +1292,14 @@ class TestEditor(OozieMockBase):
                   ],
                   response.context['params_form'].initial)
 
+  def test_trash_workflow(self):
+    previous_trashed = len(Workflow.objects.trashed())
+    assert_equal(1, len(Workflow.objects.all()))
+    response = self.c.post(reverse('oozie:delete_workflow'), {'job_selection': [self.wf.id]}, follow=True)
+    assert_equal(200, response.status_code, response)
+    assert_equal(previous_trashed + 1, len(Workflow.objects.trashed()))
+    assert_equal(0, len(Workflow.objects.all()))
+
 
 class TestEditorBundle(OozieMockBase):
 
@@ -1452,7 +1460,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(2, len(Link.objects.filter(parent__workflow=workflow)))
     assert_equal('done', Node.objects.get(workflow=workflow, node_type='end').name)
     assert_equal('uri:oozie:workflow:0.4', workflow.schema_version)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_decision(self):
@@ -1471,7 +1479,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(1, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', comment='', name='start')))
     assert_equal(1, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', name='default')))
     assert_equal(1, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', child__node_type='decisionend', name='related')))
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_decision_complex(self):
@@ -1487,7 +1495,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(0, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', comment='', name='start')))
     assert_equal(3, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', name='default')))
     assert_equal(3, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='decision', child__node_type='decisionend', name='related')))
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_distcp(self):
@@ -1503,7 +1511,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(4, len(Node.objects.filter(workflow=workflow)))
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow)))
     assert_equal('[{"type":"arg","value":"-overwrite"},{"type":"arg","value":"-m"},{"type":"arg","value":"${MAP_NUMBER}"},{"type":"arg","value":"/user/hue/oozie/workspaces/data"},{"type":"arg","value":"${OUTPUT}"}]', Node.objects.get(workflow=workflow, node_type='distcp').get_full_node().params)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_forks(self):
@@ -1518,7 +1526,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(6, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='fork')))
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='fork', name='start')))
     assert_equal(2, len(Link.objects.filter(parent__workflow=workflow, parent__node_type='fork', child__node_type='join', name='related')))
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_mapreduce(self):
@@ -1534,7 +1542,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(4, len(Node.objects.filter(workflow=workflow)))
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow)))
     assert_equal('[{"name":"mapred.reduce.tasks","value":"1"},{"name":"mapred.mapper.class","value":"org.apache.hadoop.examples.SleepJob"},{"name":"mapred.reducer.class","value":"org.apache.hadoop.examples.SleepJob"},{"name":"mapred.mapoutput.key.class","value":"org.apache.hadoop.io.IntWritable"},{"name":"mapred.mapoutput.value.class","value":"org.apache.hadoop.io.NullWritable"},{"name":"mapred.output.format.class","value":"org.apache.hadoop.mapred.lib.NullOutputFormat"},{"name":"mapred.input.format.class","value":"org.apache.hadoop.examples.SleepJob$SleepInputFormat"},{"name":"mapred.partitioner.class","value":"org.apache.hadoop.examples.SleepJob"},{"name":"mapred.speculative.execution","value":"false"},{"name":"sleep.job.map.sleep.time","value":"0"},{"name":"sleep.job.reduce.sleep.time","value":"1"}]', Node.objects.get(workflow=workflow, node_type='mapreduce').get_full_node().job_properties)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_pig(self):
@@ -1552,7 +1560,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow)))
     assert_equal('aggregate.pig', node.script_path)
     assert_equal('[{"type":"argument","value":"-param"},{"type":"argument","value":"INPUT=/user/hue/oozie/workspaces/data"},{"type":"argument","value":"-param"},{"type":"argument","value":"OUTPUT=${output}"}]', node.params)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_sqoop(self):
@@ -1570,7 +1578,7 @@ class TestImportWorkflow04(OozieMockBase):
     node = Node.objects.get(workflow=workflow, node_type='sqoop').get_full_node()
     assert_equal('["db.hsqldb.properties#db.hsqldb.properties","db.hsqldb.script#db.hsqldb.script"]', node.files)
     assert_equal('import --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir ${output} -m 1', node.script_path)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_java(self):
@@ -1593,7 +1601,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal('${output_dir}/teragen ${output_dir}/terasort', nodes[1].args)
     assert_true(nodes[0].capture_output)
     assert_false(nodes[1].capture_output)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_shell(self):
@@ -1613,7 +1621,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal('hello.py', nodes[0].command)
     assert_true(nodes[0].capture_output)
     assert_false(nodes[1].capture_output)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_fs(self):
@@ -1634,7 +1642,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal('["${nameNode}${output}/testfs","${nameNode}${output}/testfs/source"]', node.mkdirs)
     assert_equal('[{"source":"${nameNode}${output}/testfs/source","destination":"${nameNode}${output}/testfs/renamed"}]', node.moves)
     assert_equal('["${nameNode}${output}/testfs/new_file"]', node.touchzs)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_email(self):
@@ -1654,7 +1662,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal('', node.cc)
     assert_equal('I love', node.subject)
     assert_equal('Hue', node.body)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_workflow_generic(self):
@@ -1671,7 +1679,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow)))
     node = Node.objects.get(workflow=workflow, node_type='generic').get_full_node()
     assert_equal("<bleh test=\"test\">\n              <test>test</test>\n        </bleh>", node.xml)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
   def test_import_multi_kill_node(self):
@@ -1697,7 +1705,7 @@ class TestImportWorkflow04(OozieMockBase):
     assert_equal('${output_dir}/teragen ${output_dir}/terasort', nodes[1].args)
     assert_true(nodes[0].capture_output)
     assert_false(nodes[1].capture_output)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
 class TestPermissions(OozieBase):
@@ -1707,6 +1715,11 @@ class TestPermissions(OozieBase):
     self.wf = create_workflow(self.c)
     self.setup_simple_workflow()
 
+  def tearDown(self):
+    try:
+      self.wf.delete(skip_trash=True)
+    except:
+      pass
 
   def test_workflow_permissions(self):
     response = self.c.get(reverse('oozie:edit_workflow', args=[self.wf.id]))
@@ -1795,7 +1808,7 @@ class TestPermissions(OozieBase):
     finally:
       finish()
 
-    # Delete
+    # Move to trash
     finish = SHARE_JOBS.set_for_testing(False)
     try:
       response = client_not_me.post(reverse('oozie:delete_workflow'), {'job_selection': [self.wf.id]})
@@ -1806,6 +1819,28 @@ class TestPermissions(OozieBase):
     response = self.c.post(reverse('oozie:delete_workflow'), {'job_selection': [self.wf.id]}, follow=True)
     assert_equal(200, response.status_code)
 
+    # Trash
+    finish = SHARE_JOBS.set_for_testing(False)
+    try:
+      response = client_not_me.get(reverse('oozie:list_trashed_workflows'))
+      assert_false(self.wf.name in response.content, response.content)
+    finally:
+      finish()
+
+    response = self.c.get(reverse('oozie:list_trashed_workflows'))
+    assert_true(self.wf.name in response.content, response.content)
+
+    # Restore
+    finish = SHARE_JOBS.set_for_testing(False)
+    try:
+      response = client_not_me.post(reverse('oozie:restore_workflow'), {'job_selection': [coord.id]})
+      assert_true('Permission denied' in response.content, response.content)
+    finally:
+      finish()
+
+    response = self.c.post(reverse('oozie:restore_workflow'), {'job_selection': [coord.id]}, follow=True)
+    assert_equal(200, response.status_code)
+
 
   def test_coordinator_permissions(self):
     coord = create_coordinator(self.wf, self.c)
@@ -1920,7 +1955,7 @@ class TestPermissions(OozieBase):
     finally:
       finish()
 
-    # Delete
+    # Move to trash
     finish = SHARE_JOBS.set_for_testing(False)
     try:
       response = client_not_me.post(reverse('oozie:delete_coordinator'), {'job_selection': [coord.id]})
@@ -1931,6 +1966,28 @@ class TestPermissions(OozieBase):
     response = self.c.post(reverse('oozie:delete_coordinator'), {'job_selection': [coord.id]}, follow=True)
     assert_equal(200, response.status_code)
 
+    # List trash
+    finish = SHARE_JOBS.set_for_testing(True)
+    try:
+      response = client_not_me.get(reverse('oozie:list_trash_coordinators'))
+      assert_true(coord.name in response.content, response.content)
+    finally:
+      finish()
+    finish = SHARE_JOBS.set_for_testing(False)
+
+    response = client_not_me.get(reverse('oozie:list_trash_coordinators'))
+    assert_false(coord.name in response.content, response.content)
+
+    # Restore
+    finish = SHARE_JOBS.set_for_testing(False)
+    try:
+      response = client_not_me.post(reverse('oozie:restore_coordinator'), {'job_selection': [coord.id]})
+      assert_true('Permission denied' in response.content, response.content)
+    finally:
+      finish()
+
+    response = self.c.post(reverse('oozie:restore_coordinator'), {'job_selection': [coord.id]}, follow=True)
+    assert_equal(200, response.status_code)
 
   def test_bundle_permissions(self):
     bundle = create_bundle(self.c)
@@ -2031,6 +2088,29 @@ class TestPermissions(OozieBase):
     response = self.c.post(reverse('oozie:delete_bundle'), {'job_selection': [bundle.id]}, follow=True)
     assert_equal(200, response.status_code)
 
+    # List trash
+    finish = SHARE_JOBS.set_for_testing(True)
+    try:
+      response = client_not_me.get(reverse('oozie:list_trash_bundles'))
+      assert_true(bundle.name in response.content, response.content)
+    finally:
+      finish()
+    finish = SHARE_JOBS.set_for_testing(False)
+
+    response = client_not_me.get(reverse('oozie:list_trash_bundles'))
+    assert_false(bundle.name in response.content, response.content)
+
+    # Restore
+    finish = SHARE_JOBS.set_for_testing(False)
+    try:
+      response = client_not_me.post(reverse('oozie:restore_bundle'), {'job_selection': [bundle.id]})
+      assert_true('Permission denied' in response.content, response.content)
+    finally:
+      finish()
+
+    response = self.c.post(reverse('oozie:restore_bundle'), {'job_selection': [bundle.id]}, follow=True)
+    assert_equal(200, response.status_code)
+
 
 class TestEditorWithOozie(OozieBase):
 
@@ -2043,7 +2123,10 @@ class TestEditorWithOozie(OozieBase):
 
 
   def tearDown(self):
-    self.wf.delete()
+    try:
+      self.wf.delete(skip_trash=True)
+    except:
+      pass
 
 
   def test_create_workflow(self):
@@ -2095,6 +2178,14 @@ class TestEditorWithOozie(OozieBase):
     fh.close()
     assert_equal(workflow_count + 1, Workflow.objects.count(), response)
 
+  def test_delete_workflow(self):
+    previous_trashed = len(Workflow.objects.trashed())
+    assert_equal(1, len(Workflow.objects.all()))
+    response = self.c.post(reverse('oozie:delete_workflow') + "?skip_trash=true", {'job_selection': [self.wf.id]}, follow=True)
+    assert_equal(200, response.status_code, response)
+    assert_equal(previous_trashed + 1, len(Workflow.objects.trashed()))
+    assert_equal(0, len(Workflow.objects.all()))
+
 
 class TestImportWorkflow04WithOozie(OozieBase):
 
@@ -2110,7 +2201,7 @@ class TestImportWorkflow04WithOozie(OozieBase):
 
 
   def tearDown(self):
-    self.wf.delete()
+    self.wf.delete(skip_trash=True)
 
     # Make sure examples are owned by Hue.
     # This user is created by OozieBase.
@@ -2131,7 +2222,7 @@ class TestImportWorkflow04WithOozie(OozieBase):
     assert_equal(4, len(Link.objects.filter(parent__workflow=workflow)))
     node = Node.objects.get(workflow=workflow, node_type='subworkflow').get_full_node()
     assert_equal(True, node.propagate_configuration)
-    workflow.delete()
+    workflow.delete(skip_trash=True)
 
 
 class TestOozieSubmissions(OozieBase):

+ 6 - 0
apps/oozie/src/oozie/urls.py

@@ -25,18 +25,22 @@ urlpatterns = patterns(
   'oozie.views.editor',
 
   url(r'^list_workflows/$', 'list_workflows', name='list_workflows'),
+  url(r'^list_trashed_workflows/$', 'list_trashed_workflows', name='list_trashed_workflows'),
   url(r'^create_workflow/$', 'create_workflow', name='create_workflow'),
   url(r'^edit_workflow/(?P<workflow>\d+)$', 'edit_workflow', name='edit_workflow'),
   url(r'^delete_workflow$', 'delete_workflow', name='delete_workflow'),
+  url(r'^restore_workflow/$', 'restore_workflow', name='restore_workflow'),
   url(r'^clone_workflow/(?P<workflow>\d+)$', 'clone_workflow', name='clone_workflow'),
   url(r'^submit_workflow/(?P<workflow>\d+)$', 'submit_workflow', name='submit_workflow'),
   url(r'^schedule_workflow/(?P<workflow>\d+)$', 'schedule_workflow', name='schedule_workflow'),
   url(r'^import_workflow/$', 'import_workflow', name='import_workflow'),
 
   url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
+  url(r'^list_trashed_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_trashed_coordinators', name='list_trashed_coordinators'),
   url(r'^create_coordinator/(?P<workflow>[-\w]+)?$', 'create_coordinator', name='create_coordinator'),
   url(r'^edit_coordinator/(?P<coordinator>[-\w]+)$', 'edit_coordinator', name='edit_coordinator'),
   url(r'^delete_coordinator$', 'delete_coordinator', name='delete_coordinator'),
+  url(r'^restore_coordinator$', 'restore_coordinator', name='restore_coordinator'),
   url(r'^clone_coordinator/(?P<coordinator>\d+)$', 'clone_coordinator', name='clone_coordinator'),
   url(r'^create_coordinator_dataset/(?P<coordinator>[-\w]+)$', 'create_coordinator_dataset', name='create_coordinator_dataset'),
   url(r'^edit_coordinator_dataset/(?P<dataset>\d+)$', 'edit_coordinator_dataset', name='edit_coordinator_dataset'),
@@ -44,11 +48,13 @@ urlpatterns = patterns(
   url(r'^submit_coordinator/(?P<coordinator>\d+)$', 'submit_coordinator', name='submit_coordinator'),
 
   url(r'^list_bundles$', 'list_bundles', name='list_bundles'),
+  url(r'^list_trashed_bundles$', 'list_trashed_bundles', name='list_trashed_bundles'),
   url(r'^create_bundle$', 'create_bundle', name='create_bundle'),
   url(r'^edit_bundle/(?P<bundle>\d+)$', 'edit_bundle', name='edit_bundle'),
   url(r'^submit_bundle/(?P<bundle>\d+)$', 'submit_bundle', name='submit_bundle'),
   url(r'^clone_bundle/(?P<bundle>\d+)$', 'clone_bundle', name='clone_bundle'),
   url(r'^delete_bundle$', 'delete_bundle', name='delete_bundle'),
+  url(r'^restore_bundle$', 'restore_bundle', name='restore_bundle'),
   url(r'^create_bundled_coordinator/(?P<bundle>\d+)$', 'create_bundled_coordinator', name='create_bundled_coordinator'),
   url(r'^edit_bundled_coordinator/(?P<bundle>\d+)/(?P<bundled_coordinator>\d+)$', 'edit_bundled_coordinator', name='edit_bundled_coordinator'),
 

+ 122 - 10
apps/oozie/src/oozie/views/editor.py

@@ -56,7 +56,6 @@ LOG = logging.getLogger(__name__)
 
 
 def list_workflows(request):
-  show_setup_app = True
   data = Workflow.objects.filter(managed=True)
 
   if not SHARE_JOBS.get() and not request.user.is_superuser:
@@ -69,7 +68,22 @@ def list_workflows(request):
   return render('editor/list_workflows.mako', request, {
     'jobs': list(data),
     'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
-    'show_setup_app': show_setup_app,
+  })
+
+
+def list_trashed_workflows(request):
+  data = Workflow.objects.trashed().filter(managed=True)
+
+  if not SHARE_JOBS.get() and not request.user.is_superuser:
+    data = data.filter(owner=request.user)
+  else:
+    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
+
+  data = data.order_by('-last_modified')
+
+  return render('editor/list_trashed_workflows.mako', request, {
+    'jobs': list(data),
+    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
   })
 
 
@@ -91,6 +105,24 @@ def list_coordinators(request, workflow_id=None):
   })
 
 
+def list_trashed_coordinators(request, workflow_id=None):
+  data = Coordinator.objects.trashed()
+  if workflow_id is not None:
+    data = data.filter(workflow__id=workflow_id)
+
+  if not SHARE_JOBS.get() and not request.user.is_superuser:
+    data = data.filter(owner=request.user)
+  else:
+    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
+
+  data = data.order_by('-last_modified')
+
+  return render('editor/list_trashed_coordinators.mako', request, {
+    'jobs': list(data),
+    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+  })
+
+
 def list_bundles(request):
   data = Bundle.objects
 
@@ -107,6 +139,22 @@ def list_bundles(request):
   })
 
 
+def list_trashed_bundles(request):
+  data = Bundle.objects.trashed()
+
+  if not SHARE_JOBS.get() and not request.user.is_superuser:
+    data = data.filter(owner=request.user)
+  else:
+    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
+
+  data = data.order_by('-last_modified')
+
+  return render('editor/list_trashed_bundles.mako', request, {
+    'jobs': list(data),
+    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+  })
+
+
 def create_workflow(request):
   workflow = Workflow.objects.new_workflow(request.user)
 
@@ -196,19 +244,42 @@ def edit_workflow(request, workflow):
   })
 
 
-
 def delete_workflow(request):
   if request.method != 'POST':
     raise PopupException(_('A POST request is required.'))
 
+  skip_trash = 'skip_trash' in request.GET
+
   job_ids = request.POST.getlist('job_selection')
 
   for job_id in job_ids:
     job = Job.objects.is_accessible_or_exception(request, job_id)
     Job.objects.can_edit_or_exception(request, job)
-    Workflow.objects.destroy(job, request.fs)
+    if skip_trash:
+      Workflow.objects.destroy(job, request.fs)
+    else:
+      job.workflow.delete(skip_trash=False)
 
-  request.info(_('Workflow(s) deleted.'))
+  if skip_trash:
+    request.info(_('Workflow(s) deleted.'))
+  else:
+    request.info(_('Workflow(s) trashed.'))
+
+  return redirect(reverse('oozie:list_workflows'))
+
+
+def restore_workflow(request):
+  if request.method != 'POST':
+    raise PopupException(_('A POST request is required.'))
+
+  job_ids = request.POST.getlist('job_selection')
+
+  for job_id in job_ids:
+    job = Job.objects.is_accessible_or_exception(request, job_id)
+    Job.objects.can_edit_or_exception(request, job)
+    job.workflow.restore()
+
+  request.info(_('Workflow(s) restored.'))
 
   return redirect(reverse('oozie:list_workflows'))
 
@@ -306,15 +377,37 @@ def delete_coordinator(request):
   if request.method != 'POST':
     raise PopupException(_('A POST request is required.'))
 
+  skip_trash = 'skip_trash' in request.GET
+
   job_ids = request.POST.getlist('job_selection')
 
   for job_id in job_ids:
     job = Job.objects.is_accessible_or_exception(request, job_id)
     Job.objects.can_edit_or_exception(request, job)
-    Submission(request.user, job, request.fs, {}).remove_deployment_dir()
-    job.delete()
+    if skip_trash:
+      Submission(request.user, job, request.fs, {}).remove_deployment_dir()
+    job.delete(skip_trash=skip_trash)
 
-  request.info(_('Coordinator(s) deleted.'))
+  if skip_trash:
+    request.info(_('Coordinator(s) deleted.'))
+  else:
+    request.info(_('Coordinator(s) trashed.'))
+
+  return redirect(reverse('oozie:list_coordinators'))
+
+
+def restore_coordinator(request):
+  if request.method != 'POST':
+    raise PopupException(_('A POST request is required.'))
+
+  job_ids = request.POST.getlist('job_selection')
+
+  for job_id in job_ids:
+    job = Job.objects.is_accessible_or_exception(request, job_id)
+    Job.objects.can_edit_or_exception(request, job)
+    job.restore()
+
+  request.info(_('Coordinator(s) restored.'))
 
   return redirect(reverse('oozie:list_coordinators'))
 
@@ -554,19 +647,38 @@ def delete_bundle(request):
   if request.method != 'POST':
     raise PopupException(_('A POST request is required.'))
 
+  skip_trash = 'skip_trash' in request.GET
+
   job_ids = request.POST.getlist('job_selection')
 
   for job_id in job_ids:
     job = Job.objects.is_accessible_or_exception(request, job_id)
     Job.objects.can_edit_or_exception(request, job)
-    Submission(request.user, job, request.fs, {}).remove_deployment_dir()
-    job.delete()
+    if skip_trash:
+      Submission(request.user, job, request.fs, {}).remove_deployment_dir()
+    job.delete(skip_trash=skip_trash)
 
   request.info(_('Bundle(s) deleted.'))
 
   return redirect(reverse('oozie:list_bundles'))
 
 
+def restore_bundle(request):
+  if request.method != 'POST':
+    raise PopupException(_('A POST request is required.'))
+
+  job_ids = request.POST.getlist('job_selection')
+
+  for job_id in job_ids:
+    job = Job.objects.is_accessible_or_exception(request, job_id)
+    Job.objects.can_edit_or_exception(request, job)
+    job.restore()
+
+  request.info(_('Bundle(s) restored.'))
+
+  return redirect(reverse('oozie:list_bundles'))
+
+
 @check_job_access_permission()
 @check_job_edition_permission(True)
 def edit_bundle(request, bundle):