Prechádzať zdrojové kódy

[oozie] Add workflow and coordinator variables

Added a new 'parameters' field to the Job model
Refactored all the submissions views and templates
Submission popup now integrates job and actions variables
Refactored properties form and made it pluggable
Create a new template for the submission popup
Integration with resubmissions views
Added new tests
Removed all the tabs and corrected some indentations
Got rid of all js and added boostrap modals to the popups
Romain Rigaux 13 rokov pred
rodič
commit
941ab54e7e

+ 12 - 5
apps/oozie/src/oozie/forms.py

@@ -15,7 +15,6 @@
 # See the License for the specific language governing permissions and
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # limitations under the License.
 
 
-import datetime
 import logging
 import logging
 
 
 from django import forms
 from django import forms
@@ -27,6 +26,11 @@ from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
 
 
+class ParameterForm(forms.Form):
+  name = forms.CharField(max_length=40, widget=forms.widgets.HiddenInput())
+  value = forms.CharField(max_length=40, required=False)
+
+
 class WorkflowForm(forms.ModelForm):
 class WorkflowForm(forms.ModelForm):
   class Meta:
   class Meta:
     model = Workflow
     model = Workflow
@@ -117,15 +121,17 @@ class DefaultLinkForm(forms.ModelForm):
     super(DefaultLinkForm, self).__init__(*args, **kwargs)
     super(DefaultLinkForm, self).__init__(*args, **kwargs)
     self.fields['child'].widget = forms.Select(choices=((node.id, node) for node in set(workflow.node_set.all())))
     self.fields['child'].widget = forms.Select(choices=((node.id, node) for node in set(workflow.node_set.all())))
 
 
+
 DATE_FORMAT = '%m/%d/%Y'
 DATE_FORMAT = '%m/%d/%Y'
 TIME_FORMAT = '%I:%M %p'
 TIME_FORMAT = '%I:%M %p'
 
 
 class CoordinatorForm(forms.ModelForm):
 class CoordinatorForm(forms.ModelForm):
-  """Used for specifying a design"""
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
-                                   widget=SplitDateTimeWidget(attrs={'class': 'short', 'id': 'coordinator_start'}, date_format=DATE_FORMAT, time_format=TIME_FORMAT))
+                                   widget=SplitDateTimeWidget(attrs={'class': 'short', 'id': 'coordinator_start'},
+                                                              date_format=DATE_FORMAT, time_format=TIME_FORMAT))
   end = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
   end = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
-                                 widget=SplitDateTimeWidget(attrs={'class': 'short', 'id': 'coordinator_end'}, date_format=DATE_FORMAT, time_format=TIME_FORMAT))
+                                 widget=SplitDateTimeWidget(attrs={'class': 'short', 'id': 'coordinator_end'},
+                                                            date_format=DATE_FORMAT, time_format=TIME_FORMAT))
 
 
   class Meta:
   class Meta:
     model = Coordinator
     model = Coordinator
@@ -137,7 +143,8 @@ class CoordinatorForm(forms.ModelForm):
 
 
 class DatasetForm(forms.ModelForm):
 class DatasetForm(forms.ModelForm):
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
-                                   widget=SplitDateTimeWidget(attrs={'class': 'short'}, date_format=DATE_FORMAT, time_format=TIME_FORMAT))
+                                   widget=SplitDateTimeWidget(attrs={'class': 'short'},
+                                                              date_format=DATE_FORMAT, time_format=TIME_FORMAT))
 
 
   class Meta:
   class Meta:
     model = Dataset
     model = Dataset

+ 14 - 3
apps/oozie/src/oozie/models.py

@@ -64,6 +64,8 @@ class Job(models.Model):
   schema_version = models.CharField(max_length=128, blank=True, default='')
   schema_version = models.CharField(max_length=128, blank=True, default='')
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_('HDFS deployment directory'))
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_('HDFS deployment directory'))
   is_shared = models.BooleanField(default=False, db_index=True)
   is_shared = models.BooleanField(default=False, db_index=True)
+  parameters = models.TextField(default='[]',
+                                help_text=_t('Configuration parameters (e.g. market=US)'))
 
 
   unique_together = ('owner', 'name')
   unique_together = ('owner', 'name')
 
 
@@ -97,6 +99,9 @@ class Job(models.Model):
   def get_absolute_url(self):
   def get_absolute_url(self):
     return self.get_full_node().get_absolute_url()
     return self.get_full_node().get_absolute_url()
 
 
+  def get_parameters(self):
+    return json.loads(self.parameters)
+
   @property
   @property
   def status(self):
   def status(self):
     if self.is_shared:
     if self.is_shared:
@@ -104,8 +109,12 @@ class Job(models.Model):
     else:
     else:
       return 'personal'
       return 'personal'
 
 
-  def find_parameters(self, fields=None):
-    return find_parameters(self, fields)
+  def find_all_parameters(self):
+    params = dict([(param, '') for param in self.find_parameters()])
+    for param in self.get_parameters():
+      params[param['name'].strip()] = param['value']
+
+    return  [{'name': name, 'value': value} for name, value in params.iteritems()]
 
 
 
 
 class WorkflowManager(models.Manager):
 class WorkflowManager(models.Manager):
@@ -927,6 +936,8 @@ class Coordinator(Job):
     for dataset in self.dataset_set.all():
     for dataset in self.dataset_set.all():
       params.update(set(find_parameters(dataset, ['uri'])))
       params.update(set(find_parameters(dataset, ['uri'])))
 
 
+    params.update(set(self.workflow.find_parameters()))
+
     return list(params - set(['MINUTE', 'DAY', 'MONTH', 'YEAR']))
     return list(params - set(['MINUTE', 'DAY', 'MONTH', 'YEAR']))
 
 
 
 
@@ -1041,7 +1052,7 @@ class History(models.Model):
       history = History.objects.get(oozie_job_id=oozie_id)
       history = History.objects.get(oozie_job_id=oozie_id)
       if history.job.owner != user:
       if history.job.owner != user:
         history = None
         history = None
-    except History.DoesNotExist, ex:
+    except History.DoesNotExist:
       pass
       pass
 
 
     return history
     return history

+ 7 - 6
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinator.mako

@@ -76,9 +76,10 @@ ${ layout.menubar(section='dashboard') }
           <tr>
           <tr>
             <td>${ _('Manage') }</td>
             <td>${ _('Manage') }</td>
             <td>
             <td>
-            <form action="${ url('oozie:resubmit_coordinator', job_id=oozie_coordinator.id) }" method="post">
+            <form action="${ url('oozie:resubmit_coordinator', oozie_coord_id=oozie_coordinator.id) }" method="post">
             % if oozie_coordinator.is_running():
             % if oozie_coordinator.is_running():
-              <button type="button" class="btn manage-oozie-job-btn" data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_coordinator.id, action='kill') }"  data-message="The coordinator was killed!">
+              <button type="button" class="btn manage-oozie-job-btn" data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_coordinator.id, action='kill') }"
+                data-message="The coordinator was killed!">
                 ${ _('Kill') }
                 ${ _('Kill') }
               </button>
               </button>
             % else:
             % else:
@@ -95,10 +96,10 @@ ${ layout.menubar(section='dashboard') }
 
 
     <ul class="nav nav-tabs">
     <ul class="nav nav-tabs">
       <li class="active"><a href="#calendar" data-toggle="tab">${ _('Calendar') }</a></li>
       <li class="active"><a href="#calendar" data-toggle="tab">${ _('Calendar') }</a></li>
-	  <li><a href="#actions" data-toggle="tab">${ _('Actions') }</a></li>
-	  <li><a href="#configuration" data-toggle="tab">${ _('Configuration') }</a></li>
-	  <li><a href="#log" data-toggle="tab">${ _('Log') }</a></li>
-	  <li><a href="#definition" data-toggle="tab">${ _('Definition') }</a></li>
+    <li><a href="#actions" data-toggle="tab">${ _('Actions') }</a></li>
+    <li><a href="#configuration" data-toggle="tab">${ _('Configuration') }</a></li>
+    <li><a href="#log" data-toggle="tab">${ _('Log') }</a></li>
+    <li><a href="#definition" data-toggle="tab">${ _('Definition') }</a></li>
     </ul>
     </ul>
 
 
     <div class="tab-content" style="padding-bottom:200px">
     <div class="tab-content" style="padding-bottom:200px">

+ 3 - 3
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinators.mako

@@ -45,9 +45,9 @@ ${layout.menubar(section='dashboard')}
         </span>
         </span>
         <span style="float:left;padding-left:10px;padding-right:10px;margin-top:3px">${ _('days with status') }</span>
         <span style="float:left;padding-left:10px;padding-right:10px;margin-top:3px">${ _('days with status') }</span>
         <span class="btn-group" style="float:left;">
         <span class="btn-group" style="float:left;">
-	      <a class="btn btn-status btn-success">Succeeded</a>
-	      <a class="btn btn-status btn-warning">Running</a>
-	      <a class="btn btn-status btn-danger">Killed</a>
+        <a class="btn btn-status btn-success">Succeeded</a>
+        <a class="btn btn-status btn-warning">Running</a>
+        <a class="btn btn-status btn-danger">Killed</a>
         </span>
         </span>
    </form>
    </form>
   </div>
   </div>

+ 3 - 2
apps/oozie/src/oozie/templates/dashboard/list_oozie_workflow.mako

@@ -112,9 +112,10 @@ ${ layout.menubar(section='dashboard') }
       ${ _('Manage') }
       ${ _('Manage') }
     </div>
     </div>
     <div class="span3">
     <div class="span3">
-      <form action="${ url('oozie:resubmit_workflow', job_id=oozie_workflow.id) }" method="post">
+      <form action="${ url('oozie:resubmit_workflow', oozie_wf_id=oozie_workflow.id) }" method="post">
       % if oozie_workflow.is_running():
       % if oozie_workflow.is_running():
-        <button type="button" class="btn manage-oozie-job-btn" data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_workflow.id, action='kill') }" data-message="The workflow was killed!">
+        <button type="button" class="btn manage-oozie-job-btn" data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_workflow.id, action='kill') }"
+            data-message="The workflow was killed!">
           ${ _('Kill') }
           ${ _('Kill') }
         </button>
         </button>
       % else:
       % else:

+ 12 - 12
apps/oozie/src/oozie/templates/editor/create_workflow.mako

@@ -46,14 +46,14 @@ ${ layout.menubar(section='workflows') }
                    ${ utils.render_field(workflow_form['name']) }
                    ${ utils.render_field(workflow_form['name']) }
                    ${ utils.render_field(workflow_form['description']) }
                    ${ utils.render_field(workflow_form['description']) }
 
 
-					<div class="control-group ">
-					  <label class="control-label">
-					    <a href="#" id="advanced-btn" onclick="$('#advanced-container').toggle('hide')">
-					      <i class="icon-share-alt"></i> ${ _('advanced') }</a>
-					  </label>
-					  <div class="controls">					
-					  </div>				
-					</div>
+          <div class="control-group ">
+            <label class="control-label">
+              <a href="#" id="advanced-btn" onclick="$('#advanced-container').toggle('hide')">
+                <i class="icon-share-alt"></i> ${ _('advanced') }</a>
+            </label>
+            <div class="controls">
+            </div>
+          </div>
 
 
                    <div id="advanced-container" class="hide">
                    <div id="advanced-container" class="hide">
                      ${ utils.render_field(workflow_form['deployment_dir']) }
                      ${ utils.render_field(workflow_form['deployment_dir']) }
@@ -66,10 +66,10 @@ ${ layout.menubar(section='workflows') }
         </div>
         </div>
       </div>
       </div>
 
 
-	    <div class="form-actions center">
-	      <a class="btn" onclick="history.back()">${ _('Back') }</a>
-	      <input class="btn btn-primary" type="submit" value="${ _('Save') }"></input>
-	    </div>
+      <div class="form-actions center">
+        <a class="btn" onclick="history.back()">${ _('Back') }</a>
+        <input class="btn btn-primary" type="submit" value="${ _('Save') }"></input>
+      </div>
       </form>
       </form>
     </div>
     </div>
 </div>
 </div>

+ 6 - 9
apps/oozie/src/oozie/templates/editor/edit_coordinator.mako

@@ -21,6 +21,7 @@
 
 
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
+<%namespace name="properties" file="job_action_properties.mako" />
 
 
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ layout.menubar(section='coordinators') }
 ${ layout.menubar(section='coordinators') }
@@ -45,12 +46,7 @@ ${ layout.menubar(section='coordinators') }
     % endif
     % endif
   </ul>
   </ul>
 
 
-% if coordinator.id:
-  <form class="form-horizontal" id="workflowForm" action="${ url('oozie:edit_coordinator', coordinator=coordinator.id) }" method="POST">
-  % else:
-  <form class="form-horizontal" id="workflowForm" action="${ url('oozie:edit_coordinator') }" method="POST">
-  % endif
-
+  <form class="form-horizontal" id="jobForm" action="${ url('oozie:edit_coordinator', coordinator=coordinator.id) }" method="POST">
     <div class="tab-content">
     <div class="tab-content">
       <div class="tab-pane active" id="editor">
       <div class="tab-pane active" id="editor">
         <div class="row-fluid">
         <div class="row-fluid">
@@ -63,6 +59,7 @@ ${ layout.menubar(section='coordinators') }
                ${ utils.render_field(coordinator_form['description']) }
                ${ utils.render_field(coordinator_form['description']) }
                ${ utils.render_field(coordinator_form['workflow']) }
                ${ utils.render_field(coordinator_form['workflow']) }
                ${ utils.render_field(coordinator_form['is_shared']) }
                ${ utils.render_field(coordinator_form['is_shared']) }
+               ${ properties.print_key_value(_('Parameters'), 'parameters', coordinator_form, parameters) }
              </div>
              </div>
 
 
             <hr/>
             <hr/>
@@ -307,7 +304,7 @@ ${ layout.menubar(section='coordinators') }
   <div class="form-actions center">
   <div class="form-actions center">
     <a href="${ url('oozie:list_coordinator') }" class="btn">${ _('Back') }</a>
     <a href="${ url('oozie:list_coordinator') }" class="btn">${ _('Back') }</a>
     % if can_edit_coordinator:
     % if can_edit_coordinator:
-      <input class="btn btn-primary" type="submit" value="${ _('Save') }"></input>
+      <input class="btn btn-primary" data-bind="click: submit" type="submit" value="${ _('Save') }"></input>
     % endif
     % endif
   </div>
   </div>
 
 
@@ -384,8 +381,8 @@ ${ layout.menubar(section='coordinators') }
       endTime: '23:59',
       endTime: '23:59',
       step: 60
       step: 60
     };
     };
-    $( "input.date" ).datepicker();
-    $( "input.time" ).timePicker(timeOptions);
+    $("input.date").datepicker();
+    $("input.time").timePicker(timeOptions);
 
 
     $("#datasets-btn").click(function() {
     $("#datasets-btn").click(function() {
       $('[href=#datasets]').tab('show');
       $('[href=#datasets]').tab('show');

+ 12 - 11
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -21,6 +21,7 @@
 
 
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
+<%namespace name="properties" file="job_action_properties.mako" />
 
 
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ layout.menubar(section='workflows') }
 ${ layout.menubar(section='workflows') }
@@ -47,7 +48,7 @@ ${ layout.menubar(section='workflows') }
     <li><a href="#history" data-toggle="tab">${ _('History') }</a></li>
     <li><a href="#history" data-toggle="tab">${ _('History') }</a></li>
   </ul>
   </ul>
 
 
-  <form class="form-horizontal" id="workflowForm" action="${ url('oozie:edit_workflow', workflow=workflow.id) }" method="POST">
+  <form class="form-horizontal" id="jobForm" action="${ url('oozie:edit_workflow', workflow=workflow.id) }" method="POST">
 
 
     <div class="tab-content">
     <div class="tab-content">
       <div class="tab-pane active" id="editor">
       <div class="tab-pane active" id="editor">
@@ -117,13 +118,11 @@ ${ layout.menubar(section='workflows') }
             <h2>${ _('Properties') }</h2>
             <h2>${ _('Properties') }</h2>
             <br/>
             <br/>
               <fieldset>
               <fieldset>
-               ${ utils.render_field(workflow_form['name']) }
-               ${ utils.render_field(workflow_form['description']) }
-                <div class="control-group">
-                  <label class="control-label">${ _('Properties') }</label><div class="controls"></div>
-                </div>
+                ${ utils.render_field(workflow_form['name']) }
+                ${ utils.render_field(workflow_form['description']) }
                 ${ utils.render_field(workflow_form['deployment_dir']) }
                 ${ utils.render_field(workflow_form['deployment_dir']) }
                 ${ utils.render_field(workflow_form['is_shared']) }
                 ${ utils.render_field(workflow_form['is_shared']) }
+                ${ properties.print_key_value(_('Parameters'), 'parameters', workflow_form, parameters) }
              </fieldset>
              </fieldset>
            </div>
            </div>
         </div>
         </div>
@@ -166,13 +165,15 @@ ${ layout.menubar(section='workflows') }
 <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
 
 
 <script type="text/javascript">
 <script type="text/javascript">
-  $(".action-link").click(function(){
-    window.location = $(this).attr('data-edit');
-  });
+  $(document).ready(function(){
+    $(".action-link").click(function(){
+      window.location = $(this).attr('data-edit');
+    });
 
 
-  $("a[data-row-selector='true']").jHueRowSelector();
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
 </script>
 </script>
 
 
 ${ utils.path_chooser_libs(True) }
 ${ utils.path_chooser_libs(True) }
 
 
-${commonfooter(messages)}
+${ commonfooter(messages) }

+ 17 - 17
apps/oozie/src/oozie/templates/editor/edit_workflow_action.mako

@@ -347,16 +347,16 @@ ${ layout.menubar(section='workflows') }
         };
         };
       };
       };
 
 
-	  var viewModel = new ViewModel(
-	            ${ job_properties },
-	            arrayToDictArray(${ files }),
-	            arrayToDictArray(${ archives }),
-	            ${ params });
-	
-	  ko.bindingHandlers.fileChooser = {
-	        init: function(element, valueAccessor, allBindings, model) {
-	        var self = $(element);
-	        self.after(getFileBrowseButton(self));
+    var viewModel = new ViewModel(
+              ${ job_properties },
+              arrayToDictArray(${ files }),
+              arrayToDictArray(${ archives }),
+              ${ params });
+
+    ko.bindingHandlers.fileChooser = {
+          init: function(element, valueAccessor, allBindings, model) {
+          var self = $(element);
+          self.after(getFileBrowseButton(self));
       }
       }
     };
     };
 
 
@@ -371,13 +371,13 @@ ${ layout.menubar(section='workflows') }
       return $("<button>").addClass("btn").addClass("fileChooserBtn").text("..").click(function(e){
       return $("<button>").addClass("btn").addClass("fileChooserBtn").text("..").click(function(e){
         e.preventDefault();
         e.preventDefault();
         $("#fileChooserModal").jHueFileChooser({
         $("#fileChooserModal").jHueFileChooser({
-	        onFileChoose: function(filePath) {
-	            inputElement.val(filePath);
-	            $("#chooseFile").modal("hide");
-	        },
-	        createFolder: false
-	      });
-	    $("#chooseFile").modal("show");
+          onFileChoose: function(filePath) {
+              inputElement.val(filePath);
+              $("#chooseFile").modal("hide");
+          },
+          createFolder: false
+        });
+      $("#chooseFile").modal("show");
       })
       })
     }
     }
 
 

+ 36 - 36
apps/oozie/src/oozie/templates/editor/edit_workflow_fork.mako

@@ -64,7 +64,7 @@ ${ layout.menubar(section='workflows') }
       <div class="control-group">
       <div class="control-group">
         <label class="control-label"></label>
         <label class="control-label"></label>
         <div class="controls">
         <div class="controls">
-	        <table class="table-condensed">
+          <table class="table-condensed">
             <thead>
             <thead>
               <tr>
               <tr>
                 <th>${ _('Predicate') }</th>
                 <th>${ _('Predicate') }</th>
@@ -72,41 +72,41 @@ ${ layout.menubar(section='workflows') }
                 <th>${ _('Action') }</th>
                 <th>${ _('Action') }</th>
               </tr>
               </tr>
             </thead>
             </thead>
-	          <tbody>
-	            % for form in link_formset.forms:
-	                % for hidden in form.hidden_fields():
-	                  ${ hidden }
-	                % endfor
-	            <tr>
-	              <td>
-	                ${ utils.render_field(form['comment']) }
-	              </td>
-	              <td class="center">
-	                ${ _('go to') }
-	              </td>
-	              <td class="right">
-	                <a href="${ form.instance.child.get_full_node().get_edit_link() }" class="span3">${ form.instance.child }</a>
-	              </td>
-	            </tr>
-	            % endfor
-	              <tr>
-	                <td>
-	           <div class="control-group">
-	              <label class="control-label"></label>
-	              <div class="controls span8">
-	                  <div>${ _('default') }</div>
-	              </div>
-	            </div>
-	                </td>
-	                <td class="center">
-	                ${ _('go to') }
-	                </td>
-	                <td class="right">
-	                  ${ utils.render_field(default_link_form['child']) }
-	                </td>
-	              </tr>
-	          </tbody>
-	        </table>
+            <tbody>
+              % for form in link_formset.forms:
+                  % for hidden in form.hidden_fields():
+                    ${ hidden }
+                  % endfor
+              <tr>
+                <td>
+                  ${ utils.render_field(form['comment']) }
+                </td>
+                <td class="center">
+                  ${ _('go to') }
+                </td>
+                <td class="right">
+                  <a href="${ form.instance.child.get_full_node().get_edit_link() }" class="span3">${ form.instance.child }</a>
+                </td>
+              </tr>
+              % endfor
+                <tr>
+                  <td>
+             <div class="control-group">
+                <label class="control-label"></label>
+                <div class="controls span8">
+                    <div>${ _('default') }</div>
+                </div>
+              </div>
+                  </td>
+                  <td class="center">
+                  ${ _('go to') }
+                  </td>
+                  <td class="right">
+                    ${ utils.render_field(default_link_form['child']) }
+                  </td>
+                </tr>
+            </tbody>
+          </table>
         </div>
         </div>
       </div>
       </div>
 
 

+ 1 - 1
apps/oozie/src/oozie/templates/editor/gen/workflow-graph-editable.xml.mako

@@ -53,7 +53,7 @@
             <div class="span10">
             <div class="span10">
           % endif
           % endif
 
 
-            <span class="">${ node.node_type }</span>
+          <span class="">${ node.node_type }</span>
 
 
           <br/>
           <br/>
           ${ node.description }
           ${ node.description }

+ 92 - 0
apps/oozie/src/oozie/templates/editor/job_action_properties.mako

@@ -0,0 +1,92 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="utils" file="../../utils.inc.mako" />
+
+
+<%def name="print_key_value(label, element, form, initial_parameters)">
+  <div class="control-group">
+    <label class="control-label">${ label }</label>
+    <div class="controls">
+      <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
+        <thead>
+          <tr>
+            <th>${ _('Name') }</th>
+            <th>${ _('Value') }</th>
+            <th/>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: ${ element }">
+          <tr>
+            <td><input class="required" data-bind="value: name, uniqueName: false" /></td>
+            <td><input class="required" data-bind="value: value, uniqueName: false" /></td>
+            <td><a class="btn btn-small" href="#" data-bind="click: $root.remove_${ element }">${ _('Delete') }</a></td>
+          </tr>
+        </tbody>
+      </table>
+      % if len(form[element].errors):
+        <div class="row">
+          <div class="alert alert-error">
+            ${ unicode(form[element].errors) | n }
+          </div>
+        </div>
+      % endif
+
+      <button class="btn" data-bind="click: add_${ element }">${ _('Add') }</button>
+    </div>
+  </div>
+
+  <style>
+    .designTable th {
+      text-align:left;
+    }
+  </style>
+
+  <script type="text/javascript">
+    $(document).ready(function(){
+      var ViewModel = function(parameters) {
+        var self = this;
+        self.parameters = ko.observableArray(parameters);
+
+        self.add_parameters = function() {
+          self.parameters.push({name: "", value: ""});
+        };
+
+        self.remove_parameters = function(val) {
+          self.parameters.remove(val);
+        };
+
+        self.submit = function(form) {
+          var form = $("#jobForm");
+
+          $("<input>").attr("type", "hidden")
+              .attr("name", "parameters")
+              .attr("value", ko.utils.stringifyJson(self.parameters))
+              .appendTo(form);
+
+          form.submit();
+        };
+      };
+
+      var viewModel = new ViewModel(${ initial_parameters });
+      ko.applyBindings(viewModel);
+    });
+  </script>
+</%def>

+ 39 - 84
apps/oozie/src/oozie/templates/editor/list_coordinators.mako

@@ -27,6 +27,7 @@
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ layout.menubar(section='coordinators') }
 ${ layout.menubar(section='coordinators') }
 
 
+
 <div class="container-fluid">
 <div class="container-fluid">
   <h1>${ _('Coordinator Editor') }</h1>
   <h1>${ _('Coordinator Editor') }</h1>
 
 
@@ -38,15 +39,15 @@ ${ layout.menubar(section='coordinators') }
     <div class="row-fluid">
     <div class="row-fluid">
       <div class="span3">
       <div class="span3">
         <form class="form-search">
         <form class="form-search">
-            ${ _('Filter:') }
-            <input id="filterInput" class="input-xlarge search-query" placeholder="${ _('Search for username, name, etc...') }">
+          ${ _('Filter:') }
+          <input id="filterInput" class="input-xlarge search-query" placeholder="${ _('Search for username, name, etc...') }">
         </form>
         </form>
       </div>
       </div>
       <div class="span3">
       <div class="span3">
         <button class="btn action-buttons" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
         <button class="btn action-buttons" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
-	    &nbsp;&nbsp;&nbsp;&nbsp;
-	    <button class="btn action-buttons" id="clone-btn" disabled="disabled"><i class="icon-retweet"></i> ${ _('Copy') }</button>
-	    <button class="btn action-buttons" id="delete-btn" disabled="disabled"><i class="icon-remove"></i> ${ _('Delete') }</button>
+      &nbsp;&nbsp;&nbsp;&nbsp;
+      <button class="btn action-buttons" id="clone-btn" disabled="disabled"><i class="icon-retweet"></i> ${ _('Copy') }</button>
+      <button class="btn action-buttons" id="delete-btn" disabled="disabled"><i class="icon-remove"></i> ${ _('Delete') }</button>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -72,7 +73,6 @@ ${ layout.menubar(section='coordinators') }
           <td class=".btn-large action-column" data-row-selector-exclude="true" style="background-color: white;">
           <td class=".btn-large action-column" data-row-selector-exclude="true" style="background-color: white;">
             <input type="radio" name="action" data-row-selector-exclude="true"
             <input type="radio" name="action" data-row-selector-exclude="true"
               % if can_edit_job(currentuser, coordinator):
               % if can_edit_job(currentuser, coordinator):
-                  data-param-url="${ url('oozie:workflow_parameters', workflow=coordinator.id) }"
                   data-delete-url="${ url('oozie:delete_coordinator', coordinator=coordinator.id) }"
                   data-delete-url="${ url('oozie:delete_coordinator', coordinator=coordinator.id) }"
               % endif
               % endif
               % if can_access_job(currentuser, coordinator):
               % if can_access_job(currentuser, coordinator):
@@ -106,26 +106,9 @@ ${ layout.menubar(section='coordinators') }
 </div>
 </div>
 
 
 
 
-<div id="submitWf" class="modal hide fade">
-  <form id="submitWfForm" action="" method="POST">
-    <div class="modal-header">
-        <a href="#" class="close" data-dismiss="modal">&times;</a>
-        <h3 id="submitWfMessage">${ _('Submit this coordinator?') }</h3>
-    </div>
-    <div class="modal-body">
-      <fieldset>
-        <div id="param-container">
-        </div>
-      </fieldset>
-    </div>
-    <div class="modal-footer">
-      <a href="#" class="btn secondary hideModal">${ _('Cancel') }</a>
-      <input id="submitBtn" type="submit" class="btn primary" value="${ _('Submit') }"/>
-    </div>
-  </form>
-</div>
+<div id="submit-job-modal" class="modal hide"></div>
 
 
-<div id="deleteWf" class="modal hide fade">
+<div id="delete-job" class="modal hide">
   <form id="deleteWfForm" action="" method="POST">
   <form id="deleteWfForm" action="" method="POST">
     <div class="modal-header">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <a href="#" class="close" data-dismiss="modal">&times;</a>
@@ -133,14 +116,16 @@ ${ layout.menubar(section='coordinators') }
     </div>
     </div>
     <div class="modal-footer">
     <div class="modal-footer">
       <input type="submit" class="btn primary" value="${ _('Yes') }"/>
       <input type="submit" class="btn primary" value="${ _('Yes') }"/>
-      <a href="#" class="btn secondary hideModal">${ _('No') }</a>
+      <a href="#" class="btn secondary" data-dismiss="modal">${ _('No') }</a>
     </div>
     </div>
   </form>
   </form>
 </div>
 </div>
 
 
 
 
 <style>
 <style>
-  td .btn-large{ cursor: crosshair;  }
+  td .btn-large {
+  cursor: crosshair;
+  }
 
 
   .action-column {
   .action-column {
     cursor: auto;
     cursor: auto;
@@ -175,82 +160,52 @@ ${ layout.menubar(section='coordinators') }
         ['#clone-btn', 'data-clone-url']]
         ['#clone-btn', 'data-clone-url']]
 
 
       $.each(action_buttons, function(index) {
       $.each(action_buttons, function(index) {
-          if (select_btn.attr(this[1])) {
-            $(this[0]).removeAttr('disabled');
-          } else {
-            $(this[0]).attr("disabled", "disabled");
-          }
+        if (select_btn.attr(this[1])) {
+          $(this[0]).removeAttr('disabled');
+        } else {
+          $(this[0]).attr("disabled", "disabled");
+        }
       });
       });
     }
     }
 
 
     update_action_buttons_status();
     update_action_buttons_status();
 
 
     $("#delete-btn").click(function(e){
     $("#delete-btn").click(function(e){
-        var _this = $('input[name=action]:checked');
-        var _action = _this.attr("data-delete-url");
-        $("#deleteWfForm").attr("action", _action);
-        $("#deleteWfMessage").text(_this.attr("alt"));
-        $("#deleteWf").modal("show");
+      var _this = $('input[name=action]:checked');
+      var _action = _this.attr("data-delete-url");
+      $("#deleteWfForm").attr("action", _action);
+      $("#deleteWfMessage").text(_this.attr("alt"));
+      $("#delete-job").modal("show");
     });
     });
 
 
-    $("#submit-btn").click(function(){
-        var _this = $('input[name=action]:checked');
-        var _action = _this.attr("data-submit-url");
-        $("#submitWfForm").attr("action", _action);
-        $("#submitWfMessage").text(_this.attr("alt"));
-        // We will show the model form, but disable the submit button
-        // until we've finish loading the parameters via ajax.
-        $("#submitBtn").attr("disabled", "disabled");
-        $("#submitWf").modal("show");
-
-        $.get(_this.attr("data-param-url"), function(data) {
-            var params = data["params"]
-            var container = $("#param-container");
-            container.empty();
-
-            for (key in params) {
-                if (! params.hasOwnProperty(key)) {
-                    continue;
-                }
-                container.append(
-	              $("<div/>").addClass("clearfix")
-	                .append($("<label/>").text(params[key]))
-	                .append(
-	                    $("<div/>").addClass("input")
-	                      .append($("<input/>").attr("name", key).attr("type", "text"))
-	                )
-                )
-            }
-            // Good. We can submit now.
-            $("#submitBtn").removeAttr("disabled");
-        }, "json");
-    });
+    $('#submit-btn').click(function() {
+      var _this = $('input[name=action]:checked');
+      var _action = _this.attr("data-submit-url");
+
+      $.get(_action,  function(response) {
+          $('#submit-job-modal').html(response);
+          $('#submit-job-modal').modal('show');
+        }
+      );
+     });
 
 
     $(".deleteConfirmation").click(function(){
     $(".deleteConfirmation").click(function(){
-        var _this = $(this);
-        var _action = _this.attr("data-confirmation-url");
-        $("#deleteWfForm").attr("action", _action);
-        $("#deleteWfMessage").text(_this.attr("alt"));
-        $("#deleteWf").modal("show");
+      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){
     $("#clone-btn").click(function(e){
-        var _this = $('input[name=action]:checked');
-        var _url = _this.attr("data-clone-url");
+      var _this = $('input[name=action]:checked');
+      var _url = _this.attr("data-clone-url");
 
 
       $.post(_url, function(data) {
       $.post(_url, function(data) {
         window.location = data.url;
         window.location = data.url;
       });
       });
     });
     });
 
 
-    $("#deleteWf .hideModal").click(function(){
-        $("#deleteWf").modal("hide");
-    });
-
-    $("#submitWf .hideModal").click(function(){
-        $("#submitWf").modal("hide");
-    });
-
     var oTable = $('#coordinatorTable').dataTable( {
     var oTable = $('#coordinatorTable').dataTable( {
       "sPaginationType": "bootstrap",
       "sPaginationType": "bootstrap",
       'iDisplayLength': 50,
       'iDisplayLength': 50,

+ 49 - 96
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -76,7 +76,6 @@ ${ layout.menubar(section='workflows') }
           <td class=".btn-large action-column" data-row-selector-exclude="true" style="background-color: white;">
           <td class=".btn-large action-column" data-row-selector-exclude="true" style="background-color: white;">
             <input type="radio" name="action" data-row-selector-exclude="true"
             <input type="radio" name="action" data-row-selector-exclude="true"
               % if can_access_job(currentuser, workflow):
               % if can_access_job(currentuser, workflow):
-                  data-param-url="${ url('oozie:workflow_parameters', workflow=workflow.id) }"
                   data-submit-url="${ url('oozie:submit_workflow', workflow=workflow.id) }"
                   data-submit-url="${ url('oozie:submit_workflow', workflow=workflow.id) }"
                   data-schedule-url="${ url('oozie:create_coordinator', workflow=workflow.id) }"
                   data-schedule-url="${ url('oozie:create_coordinator', workflow=workflow.id) }"
               % endif
               % endif
@@ -108,24 +107,7 @@ ${ layout.menubar(section='workflows') }
 </div>
 </div>
 
 
 
 
-<div id="submitWf" class="modal hide fade">
-  <form id="submitWfForm" action="" method="POST">
-    <div class="modal-header">
-      <a href="#" class="close" data-dismiss="modal">&times;</a>
-      <h3 id="submitWfMessage">${ _('Submit this workflow?') }</h3>
-    </div>
-    <div class="modal-body">
-      <fieldset>
-        <div id="param-container">
-        </div>
-      </fieldset>
-    </div>
-    <div class="modal-footer">
-      <a href="#" class="btn secondary hideModal">${ _('Cancel') }</a>
-      <input id="submitBtn" type="submit" class="btn primary" value="${ _('Submit') }"/>
-    </div>
-  </form>
-</div>
+<div id="submit-wf-modal" class="modal hide"></div>
 
 
 <div id="deleteWf" class="modal hide fade">
 <div id="deleteWf" class="modal hide fade">
   <form id="deleteWfForm" action="" method="POST">
   <form id="deleteWfForm" action="" method="POST">
@@ -135,7 +117,7 @@ ${ layout.menubar(section='workflows') }
     </div>
     </div>
     <div class="modal-footer">
     <div class="modal-footer">
       <input type="submit" class="btn primary" value="${ _('Yes') }"/>
       <input type="submit" class="btn primary" value="${ _('Yes') }"/>
-      <a href="#" class="btn secondary hideModal">${ _('No') }</a>
+      <a href="#" class="btn secondary" data-dismiss="modal">${ _('No') }</a>
     </div>
     </div>
   </form>
   </form>
 </div>
 </div>
@@ -168,81 +150,60 @@ ${ layout.menubar(section='workflows') }
 
 
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function() {
   $(document).ready(function() {
-      $(".action-row").click(function(e){
-        var select_btn = $(this).find('input');
-        select_btn.prop("checked", true);
-
-        $(".action-row").css("background-color", "");
-        $(this).css("background-color", "#ECF4F8");
+    $(".action-row").click(function(e){
+      var select_btn = $(this).find('input');
+      select_btn.prop("checked", true);
 
 
-        $(".action-buttons").attr("disabled", "disabled");
+      $(".action-row").css("background-color", "");
+      $(this).css("background-color", "#ECF4F8");
 
 
-        update_action_buttons_status();
-      });
+      $(".action-buttons").attr("disabled", "disabled");
 
 
-      function update_action_buttons_status() {
-        var select_btn = $('input[name=action]:checked');
+      update_action_buttons_status();
+    });
 
 
-        var action_buttons = [
-          ['#submit-btn', 'data-submit-url'],
-          ['#schedule-btn', 'data-schedule-url'],
-          ['#delete-btn', 'data-delete-url'],
-          ['#clone-btn', 'data-clone-url']]
+    function update_action_buttons_status() {
+      var select_btn = $('input[name=action]:checked');
+
+      var action_buttons = [
+        ['#submit-btn', 'data-submit-url'],
+        ['#schedule-btn', 'data-schedule-url'],
+        ['#delete-btn', 'data-delete-url'],
+        ['#clone-btn', 'data-clone-url']]
+
+      $.each(action_buttons, function(index) {
+        if (select_btn.attr(this[1])) {
+          $(this[0]).removeAttr('disabled');
+        } else {
+          $(this[0]).attr("disabled", "disabled");
+        }
+      });
+    }
 
 
-        $.each(action_buttons, function(index) {
-          if (select_btn.attr(this[1])) {
-            $(this[0]).removeAttr('disabled');
-          } else {
-            $(this[0]).attr("disabled", "disabled");
-          }
-        });
-      }
+    update_action_buttons_status();
 
 
-      update_action_buttons_status();
+    $("#delete-btn").click(function(e){
+      var _this = $('input[name=action]:checked');
+      var _action = _this.attr("data-delete-url");
+      $("#deleteWfForm").attr("action", _action);
+      $("#deleteWfMessage").text(_this.attr("alt"));
+      $("#deleteWf").modal("show");
+    });
 
 
-      $("#delete-btn").click(function(e){
-          var _this = $('input[name=action]:checked');
-          var _action = _this.attr("data-delete-url");
-          $("#deleteWfForm").attr("action", _action);
-          $("#deleteWfMessage").text(_this.attr("alt"));
-          $("#deleteWf").modal("show");
-      });
+    $('#submit-btn').click(function() {
+      var _this = $('input[name=action]:checked');
+      var _action = _this.attr("data-submit-url");
 
 
-      $("#submit-btn").click(function(e){
-          var _this = $('input[name=action]:checked');
-          var _action = _this.attr("data-submit-url");
-          $("#submitWfForm").attr("action", _action);
-          $("#submitWfMessage").text(_this.attr("alt"));
-          // We will show the model form, but disable the submit button
-          // until we've finish loading the parameters via ajax.
-          $("#submitBtn").attr("disabled", "disabled");
-          $("#submitWf").modal("show");
-
-          $.get(_this.attr("data-param-url"), function(data) {
-              var params = data["params"]
-              var container = $("#param-container");
-              container.empty();
-              for (key in params) {
-                if (!params.hasOwnProperty(key)) {
-                    continue;
-                }
-                container.append(
-                  $("<div/>").addClass("clearfix")
-                    .append($("<label/>").text(params[key]))
-                    .append(
-                        $("<div/>").addClass("input")
-                          .append($("<input/>").attr("name", key).attr("type", "text"))
-                    )
-                )
-              }
-              // Good. We can submit now.
-              $("#submitBtn").removeAttr("disabled");
-          }, "json");
-      });
+      $.get(_action,  function(response) {
+          $('#submit-wf-modal').html(response);
+          $('#submit-wf-modal').modal('show');
+        }
+      );
+     });
 
 
     $("#clone-btn").click(function(e){
     $("#clone-btn").click(function(e){
-        var _this = $('input[name=action]:checked');
-        var _url = _this.attr("data-clone-url");
+      var _this = $('input[name=action]:checked');
+      var _url = _this.attr("data-clone-url");
 
 
       $.post(_url, function(data) {
       $.post(_url, function(data) {
         window.location = data.url;
         window.location = data.url;
@@ -250,18 +211,10 @@ ${ layout.menubar(section='workflows') }
     });
     });
 
 
     $("#schedule-btn").click(function(e){
     $("#schedule-btn").click(function(e){
-        var _this = $('input[name=action]:checked');
-        var _url = _this.attr("data-schedule-url");
-
-        window.location.replace(_url);
-    });
-
-    $("#submitWf .hideModal").click(function(){
-        $("#submitWf").modal("hide");
-    });
+      var _this = $('input[name=action]:checked');
+      var _url = _this.attr("data-schedule-url");
 
 
-    $("#deleteWf .hideModal").click(function(){
-        $("#deleteWf").modal("hide");
+      window.location.replace(_url);
     });
     });
 
 
     var oTable = $('#workflowTable').dataTable( {
     var oTable = $('#workflowTable').dataTable( {

+ 57 - 0
apps/oozie/src/oozie/templates/editor/submit_job_popup.mako

@@ -0,0 +1,57 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="utils" file="../utils.inc.mako" />
+
+
+<form action="${ action }" method="POST">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${ _('Submit this job?') }</h3>
+  </div>
+  <div class="modal-body">
+    <fieldset>
+      <div id="param-container">
+
+       ${ params_form.management_form }
+
+       % for form in params_form.forms:
+          % for hidden in form.hidden_fields():
+            ${ hidden }
+          % endfor
+          <div class="fieldWrapper">
+            <div class="row-fluid">
+              <div class="span6">
+                ${ form['name'].form.initial.get('name') }
+              </div>
+              <div class="span6">
+                ${ utils.render_field(form['value'], show_label=False) }
+              </div>
+            </div>
+          </div>
+         % endfor
+      </div>
+    </fieldset>
+  </div>
+  <div class="modal-footer">
+    <a href="#" class="btn secondary" data-dismiss="modal">${ _('Cancel') }</a>
+    <input id="submit-btn" type="submit" class="btn primary" value="${ _('Submit') }"/>
+  </div>
+</form>

+ 4 - 2
apps/oozie/src/oozie/templates/utils.inc.mako

@@ -137,11 +137,13 @@
 </%def>
 </%def>
 
 
 
 
-<%def name="render_field(field)">
+<%def name="render_field(field, show_label=True)">
   %if not field.is_hidden:
   %if not field.is_hidden:
     <% group_class = len(field.errors) and "error" or "" %>
     <% group_class = len(field.errors) and "error" or "" %>
     <div class="control-group ${group_class}">
     <div class="control-group ${group_class}">
-      <label class="control-label">${ field.label | n }</label>
+      % if show_label:
+        <label class="control-label">${ field.label | n }</label>
+      % endif
       <div class="controls">
       <div class="controls">
         ${ field }
         ${ field }
         % if len(field.errors):
         % if len(field.errors):

+ 12 - 5
apps/oozie/src/oozie/tests.py

@@ -31,7 +31,8 @@ from liboozie import oozie_api
 from liboozie.types import WorkflowList, Workflow as OozieWorkflow, Coordinator as OozieCoordinator,\
 from liboozie.types import WorkflowList, Workflow as OozieWorkflow, Coordinator as OozieCoordinator,\
   CoordinatorList, WorkflowAction
   CoordinatorList, WorkflowAction
 
 
-from oozie.models import Workflow, Node, Job, Coordinator, Fork, History
+from oozie.models import Workflow, Node, Job, Coordinator, Fork, History,\
+  find_parameters
 from oozie.conf import SHARE_JOBS, REMOTE_DEPLOYMENT_DIR
 from oozie.conf import SHARE_JOBS, REMOTE_DEPLOYMENT_DIR
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -117,7 +118,7 @@ class TestEditor:
             Job(name="foo ${b} $$"),
             Job(name="foo ${b} $$"),
             Job(name="${foo}", description="xxx ${foo}")]
             Job(name="${foo}", description="xxx ${foo}")]
 
 
-    result = [job.find_parameters(['name', 'description']) for job in jobs]
+    result = [find_parameters(job, ['name', 'description']) for job in jobs]
     assert_equal(set(["b", "foo"]), reduce(lambda x, y: x | set(y), result, set()))
     assert_equal(set(["b", "foo"]), reduce(lambda x, y: x | set(y), result, set()))
 
 
 
 
@@ -126,6 +127,10 @@ class TestEditor:
     pass
     pass
 
 
 
 
+  def test_find_all_parameters(self):
+        assert_equal([{'name': u'SLEEP', 'value': ''}, {'name': u'market', 'value': u'US'}], self.wf.find_all_parameters())
+
+
   def test_move_up(self):
   def test_move_up(self):
     action1 = Node.objects.get(name='action-name-1')
     action1 = Node.objects.get(name='action-name-1')
     action2 = Node.objects.get(name='action-name-2')
     action2 = Node.objects.get(name='action-name-2')
@@ -727,7 +732,7 @@ class TestEditor:
 
 
 
 
 # Utils
 # Utils
-WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u'']}
+WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u''], u'parameters': [u'[{"name":"market","value":"US"}]']}
 
 
 
 
 # Beware: client not consistent with self.c in TestEditor
 # Beware: client not consistent with self.c in TestEditor
@@ -735,7 +740,8 @@ def add_action(workflow, action, name):
   c = make_logged_in_client()
   c = make_logged_in_client()
 
 
   response = c.post("/oozie/new_action/%s/%s/%s" % (workflow, 'mapreduce', action), {
   response = c.post("/oozie/new_action/%s/%s/%s" % (workflow, 'mapreduce', action), {
-     u'files': [u'[]'], u'name': [name], u'jar_path': [u'/tmp/.file.jar'], u'job_properties': [u'[]'], u'archives': [u'[]'], u'description': [u'']}, follow=True)
+     u'files': [u'[]'], u'name': [name], u'jar_path': [u'/tmp/.file.jar'], u'job_properties': [u'[{"name":"sleep","value":"${SLEEP}"}]'],
+     u'archives': [u'[]'], u'description': [u'']}, follow=True)
   assert_equal(200, response.status_code)
   assert_equal(200, response.status_code)
   assert_true(Node.objects.filter(name=name).exists(), response)
   assert_true(Node.objects.filter(name=name).exists(), response)
   return Node.objects.get(name=name)
   return Node.objects.get(name=name)
@@ -775,7 +781,8 @@ def create_coordinator(workflow):
                         u'frequency_number': [u'1'], u'frequency_unit': [u'days'],
                         u'frequency_number': [u'1'], u'frequency_unit': [u'days'],
                         u'start_0': [u'07/01/2012'], u'start_1': [u'12:00 AM'],
                         u'start_0': [u'07/01/2012'], u'start_1': [u'12:00 AM'],
                         u'end_0': [u'07/04/2012'], u'end_1': [u'12:00 AM'],
                         u'end_0': [u'07/04/2012'], u'end_1': [u'12:00 AM'],
-                        u'timezone': [u'America/Los_Angeles']})
+                        u'timezone': [u'America/Los_Angeles'],
+                        u'parameters': [u'[{"name":"market","value":"US,France"}]']})
   assert_equal(coord_count + 1, Coordinator.objects.count(), response)
   assert_equal(coord_count + 1, Coordinator.objects.count(), response)
 
 
   return Coordinator.objects.get()
   return Coordinator.objects.get()

+ 2 - 3
apps/oozie/src/oozie/urls.py

@@ -30,7 +30,7 @@ urlpatterns = patterns(
   url(r'^delete_workflow/(?P<workflow>\d+)$', 'delete_workflow', name='delete_workflow'),
   url(r'^delete_workflow/(?P<workflow>\d+)$', 'delete_workflow', name='delete_workflow'),
   url(r'^clone_workflow/(?P<workflow>\d+)$', 'clone_workflow', name='clone_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'^submit_workflow/(?P<workflow>\d+)$', 'submit_workflow', name='submit_workflow'),
-  url(r'^resubmit_workflow/(?P<job_id>[-\w]+)$', 'resubmit_workflow', name='resubmit_workflow'),
+  url(r'^resubmit_workflow/(?P<oozie_wf_id>[-\w]+)$', 'resubmit_workflow', name='resubmit_workflow'),
 
 
   url(r'^new_action/(?P<workflow>\d+)/(?P<node_type>\w+)/(?P<parent_action_id>\d+)$', 'new_action', name='new_action'),
   url(r'^new_action/(?P<workflow>\d+)/(?P<node_type>\w+)/(?P<parent_action_id>\d+)$', 'new_action', name='new_action'),
   url(r'^edit_action/(?P<action>\d+)$', 'edit_action', name='edit_action'),
   url(r'^edit_action/(?P<action>\d+)$', 'edit_action', name='edit_action'),
@@ -48,9 +48,8 @@ urlpatterns = patterns(
   url(r'^create_coordinator_dataset/(?P<coordinator>[-\w]+)$', 'create_coordinator_dataset', name='create_coordinator_dataset'),
   url(r'^create_coordinator_dataset/(?P<coordinator>[-\w]+)$', 'create_coordinator_dataset', name='create_coordinator_dataset'),
   url(r'^create_coordinator_data/(?P<coordinator>[-\w]+)/(?P<data_type>(input|output))$', 'create_coordinator_data', name='create_coordinator_data'),
   url(r'^create_coordinator_data/(?P<coordinator>[-\w]+)/(?P<data_type>(input|output))$', 'create_coordinator_data', name='create_coordinator_data'),
   url(r'^submit_coordinator/(?P<coordinator>\d+)$', 'submit_coordinator', name='submit_coordinator'),
   url(r'^submit_coordinator/(?P<coordinator>\d+)$', 'submit_coordinator', name='submit_coordinator'),
-  url(r'^resubmit_coordinator/(?P<job_id>[-\w]+)$', 'resubmit_coordinator', name='resubmit_coordinator'),
+  url(r'^resubmit_coordinator/(?P<oozie_coord_id>[-\w]+)$', 'resubmit_coordinator', name='resubmit_coordinator'),
 
 
-  url(r'^workflow_parameters/(?P<workflow>\d+)$', 'get_workflow_parameters', name='workflow_parameters'),
   url(r'^list_history$', 'list_history', name='list_history'),
   url(r'^list_history$', 'list_history', name='list_history'),
   url(r'^list_history/(?P<record_id>[-\w]+)$', 'list_history_record', name='list_history_record'),
   url(r'^list_history/(?P<record_id>[-\w]+)$', 'list_history_record', name='list_history_record'),
   url(r'^setup_app/$', 'setup_app', name='setup_app'),
   url(r'^setup_app/$', 'setup_app', name='setup_app'),

+ 83 - 71
apps/oozie/src/oozie/views/editor.py

@@ -22,9 +22,10 @@ except ImportError:
 import logging
 import logging
 
 
 
 
-from django.forms.models import inlineformset_factory, modelformset_factory
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db.models import Q
 from django.db.models import Q
+from django.forms.formsets import formset_factory
+from django.forms.models import inlineformset_factory, modelformset_factory
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.utils.functional import wraps
 from django.utils.functional import wraps
@@ -42,7 +43,7 @@ from oozie.models import Workflow, Node, Link, History, Coordinator,\
   Dataset, DataInput, DataOutput, Job, _STD_PROPERTIES_JSON
   Dataset, DataInput, DataOutput, Job, _STD_PROPERTIES_JSON
 from oozie.forms import NodeForm, WorkflowForm, CoordinatorForm, DatasetForm,\
 from oozie.forms import NodeForm, WorkflowForm, CoordinatorForm, DatasetForm,\
   DataInputForm, DataInputSetForm, DataOutputForm, DataOutputSetForm, LinkForm,\
   DataInputForm, DataInputSetForm, DataOutputForm, DataOutputSetForm, LinkForm,\
-  DefaultLinkForm, design_form_by_type
+  DefaultLinkForm, design_form_by_type, ParameterForm
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -253,6 +254,7 @@ def edit_workflow(request, workflow):
     'graph': graph,
     'graph': graph,
     'history': history,
     'history': history,
     'user_can_edit_job': user_can_edit_job,
     'user_can_edit_job': user_can_edit_job,
+    'parameters': extract_field_data(workflow_form['parameters'])
   })
   })
 
 
 
 
@@ -285,58 +287,59 @@ def clone_workflow(request, workflow):
 
 
 @check_job_access_permission
 @check_job_access_permission
 def submit_workflow(request, workflow):
 def submit_workflow(request, workflow):
-  if request.method != 'POST':
-    raise PopupException(_('A POST request is required.'))
+  ParametersFormSet = formset_factory(ParameterForm, extra=0)
 
 
-  try:
-    mapping = dict(request.POST.iteritems())
-    job_id = _submit_workflow(request, workflow, mapping)
-  except RestException, ex:
-    raise PopupException(_("Error submitting workflow %s") % (workflow,),
-                         detail=ex._headers.get('oozie-error-message', ex))
-
-  request.info(_('Workflow submitted'))
-
-  return redirect(reverse('oozie:list_oozie_workflow', kwargs={'job_id': job_id}))
+  if request.method == 'POST':
+    params_form = ParametersFormSet(request.POST)
 
 
+    if params_form.is_valid():
+      mapping = dict([(param['name'], param['value']) for param in params_form.cleaned_data])
 
 
-def _submit_workflow(request, workflow, mapping):
-  submission = Submission(request.user, workflow, request.fs, mapping)
-  job_id = submission.run()
-  History.objects.create_from_submission(submission)
-  return job_id
+      job_id = _submit_workflow(request, workflow, mapping)
 
 
+      request.info(_('Workflow submitted'))
+      return redirect(reverse('oozie:list_oozie_workflow', kwargs={'job_id': job_id}))
+    else:
+      request.error(_('Invalid submission form: %s' % params_form.errors))
+  else:
+    parameters = workflow.find_all_parameters()
+    params_form = ParametersFormSet(initial=parameters)
 
 
-def resubmit_workflow(request, job_id):
-  if request.method != 'POST':
-    raise PopupException(_('A POST request is required.'))
+  popup = render('editor/submit_job_popup.mako', request, {
+                 'params_form': params_form,
+                 'action': reverse('oozie:submit_workflow', kwargs={'workflow': workflow.id})
+                 }, force_template=True).content
+  return HttpResponse(json.dumps(popup), mimetype="application/json")
 
 
-  history = History.objects.get(oozie_job_id=job_id)
 
 
-  can_access_job_or_exception(request, history.job.id)
 
 
+def _submit_workflow(request, workflow, mapping):
   try:
   try:
-    workflow = history.get_workflow().get_full_node()
-    properties = history.properties_dict
-    job_id = _submit_workflow(request, workflow, properties)
+    submission = Submission(request.user, workflow, request.fs, mapping)
+    job_id = submission.run()
+    History.objects.create_from_submission(submission)
+    return job_id
   except RestException, ex:
   except RestException, ex:
     raise PopupException(_("Error submitting workflow %s") % (workflow,),
     raise PopupException(_("Error submitting workflow %s") % (workflow,),
                          detail=ex._headers.get('oozie-error-message', ex))
                          detail=ex._headers.get('oozie-error-message', ex))
 
 
   request.info(_('Workflow submitted'))
   request.info(_('Workflow submitted'))
-
   return redirect(reverse('oozie:list_oozie_workflow', kwargs={'job_id': job_id}))
   return redirect(reverse('oozie:list_oozie_workflow', kwargs={'job_id': job_id}))
 
 
-@check_job_access_permission
-def get_workflow_parameters(request, workflow):
-  """
-  Return the parameters found in the workflow as a JSON dictionary of {param_key : label}.
-  This expects an Ajax call.
-  """
-  params = workflow.find_parameters()
 
 
-  params_with_labels = dict((p, p.upper()) for p in params)
-  return render('dont_care_for_ajax', request, { 'params': params_with_labels })
+def resubmit_workflow(request, oozie_wf_id):
+  if request.method != 'POST':
+    raise PopupException(_('A POST request is required.'))
+
+  history = History.objects.get(oozie_job_id=oozie_wf_id)
+  can_access_job_or_exception(request, history.job.id)
+
+  workflow = history.get_workflow().get_full_node()
+  properties = history.properties_dict
+  job_id = _submit_workflow(request, workflow, properties)
+
+  request.info(_('Workflow re-submitted'))
+  return redirect(reverse('oozie:list_oozie_workflow', kwargs={'job_id': job_id}))
 
 
 
 
 @check_job_access_permission
 @check_job_access_permission
@@ -561,7 +564,8 @@ def edit_coordinator(request, coordinator):
     'data_input_form': data_input_form,
     'data_input_form': data_input_form,
     'data_output_form': data_output_form,
     'data_output_form': data_output_form,
     'history': history,
     'history': history,
-    'can_edit_coordinator': can_edit_job(request.user, coordinator.workflow)
+    'can_edit_coordinator': can_edit_job(request.user, coordinator.workflow),
+    'parameters': extract_field_data(coordinator_form['parameters'])
   })
   })
 
 
 
 
@@ -647,57 +651,65 @@ def clone_coordinator(request, coordinator):
 
 
 @check_job_access_permission
 @check_job_access_permission
 def submit_coordinator(request, coordinator):
 def submit_coordinator(request, coordinator):
-  if request.method != 'POST':
-    raise PopupException(_('A POST request is required.'))
+  ParametersFormSet = formset_factory(ParameterForm, extra=0)
 
 
-  try:
-    job_id = _submit_coordinator(request, coordinator, request.POST)
-  except RestException, ex:
-    raise PopupException(_("Error submitting coordinator %s") % (coordinator,),
-                         detail=ex._headers.get('oozie-error-message', ex))
+  if request.method == 'POST':
+    params_form = ParametersFormSet(request.POST)
 
 
-  request.info(_('Coordinator submitted'))
+    if params_form.is_valid():
+      mapping = dict([(param['name'], param['value']) for param in params_form.cleaned_data])
+      job_id = _submit_coordinator(request, coordinator, mapping)
 
 
-  return redirect(reverse('oozie:list_oozie_coordinator', kwargs={'job_id': job_id}))
+      request.info(_('Coordinator submitted'))
+      return redirect(reverse('oozie:list_oozie_coordinator', kwargs={'job_id': job_id}))
+    else:
+      request.error(_('Invalid submission form: %s' % params_form.errors))
+  else:
+    parameters = coordinator.find_all_parameters()
+    params_form = ParametersFormSet(initial=parameters)
+
+  popup = render('editor/submit_job_popup.mako', request, {
+                 'params_form': params_form,
+                 'action': reverse('oozie:submit_coordinator',  kwargs={'coordinator': coordinator.id})
+                }, force_template=True).content
+  return HttpResponse(json.dumps(popup), mimetype="application/json")
 
 
 
 
 def _submit_coordinator(request, coordinator, mapping):
 def _submit_coordinator(request, coordinator, mapping):
-  if not coordinator.workflow.is_deployed(request.fs):
-    submission = Submission(request.user, coordinator.workflow, request.fs, mapping)
-    wf_dir = submission.deploy()
-    coordinator.workflow.deployment_dir = wf_dir
-    coordinator.workflow.save()
+  try:
+    if not coordinator.workflow.is_deployed(request.fs):
+      submission = Submission(request.user, coordinator.workflow, request.fs, mapping)
+      wf_dir = submission.deploy()
+      coordinator.workflow.deployment_dir = wf_dir
+      coordinator.workflow.save()
 
 
-  coordinator.deployment_dir = coordinator.workflow.deployment_dir
-  properties = {'wf_application_path': coordinator.workflow.deployment_dir}
-  properties.update(dict(request.POST.iteritems()))
+    coordinator.deployment_dir = coordinator.workflow.deployment_dir
+    properties = {'wf_application_path': request.fs.get_hdfs_path(coordinator.workflow.deployment_dir)}
+    properties.update(mapping)
 
 
-  submission = Submission(request.user, coordinator, request.fs, properties=properties)
-  job_id = submission.run()
+    submission = Submission(request.user, coordinator, request.fs, properties=properties)
+    job_id = submission.run()
 
 
-  History.objects.create_from_submission(submission)
+    History.objects.create_from_submission(submission)
 
 
-  return job_id
+    return job_id
+  except RestException, ex:
+    raise PopupException(_("Error submitting coordinator %s") % (coordinator,),
+                         detail=ex._headers.get('oozie-error-message', ex))
 
 
 
 
-def resubmit_coordinator(request, job_id):
+def resubmit_coordinator(request, oozie_coord_id):
   if request.method != 'POST':
   if request.method != 'POST':
     raise PopupException(_('A POST request is required.'))
     raise PopupException(_('A POST request is required.'))
 
 
-  history = History.objects.get(oozie_job_id=job_id)
-
+  history = History.objects.get(oozie_job_id=oozie_coord_id)
   can_access_job_or_exception(request, history.job.id)
   can_access_job_or_exception(request, history.job.id)
 
 
-  try:
-    coordinator = history.get_coordinator().get_full_node()
-    properties = history.properties_dict
-    job_id = _submit_coordinator(request, coordinator, properties)
-  except RestException, ex:
-    raise PopupException(_("Error submitting coordinator %s") % (coordinator,),
-                         detail=ex._headers.get('oozie-error-message', ex))
-
-  request.info(_('Coordinator submitted'))
+  coordinator = history.get_coordinator().get_full_node()
+  properties = history.properties_dict
+  job_id = _submit_coordinator(request, coordinator, properties)
 
 
+  request.info(_('Coordinator re-submitted'))
   return redirect(reverse('oozie:list_oozie_coordinator', kwargs={'job_id': job_id}))
   return redirect(reverse('oozie:list_oozie_coordinator', kwargs={'job_id': job_id}))
 
 
 
 

+ 1 - 1
desktop/core/src/desktop/lib/django_util.py

@@ -195,7 +195,7 @@ def render(template, request, data, json=None, template_lib=None, force_template
   template_lib), as well as as arbitrary data.
   template_lib), as well as as arbitrary data.
 
 
   It typically renders to an HttpResponse.  If the request is a non-JFrame
   It typically renders to an HttpResponse.  If the request is a non-JFrame
-  AJAX reqeust (or if data is None), it renders into JSON.
+  AJAX request (or if data is None), it renders into JSON.
 
 
   if force-template is True, will render the non-AJAX template response even if the
   if force-template is True, will render the non-AJAX template response even if the
   request is via AJAX. This is to facilitate fetching HTML fragments.
   request is via AJAX. This is to facilitate fetching HTML fragments.