Browse Source

[oozie] Improve Editor usability

Adding help_text to the model fields
Popover help text when editing a Workflow, Workflow action, Coordinator
Order history desc
Fix create Workflow/Coordinator form bug with 'parameters'
Feature: when schedule a workflow, list the already created coordinators
instead if they are some
Romain Rigaux 13 years ago
parent
commit
aad045a725

+ 3 - 1
apps/oozie/src/oozie/forms.py

@@ -38,6 +38,7 @@ class WorkflowForm(forms.ModelForm):
     widgets = {
     widgets = {
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'deployment_dir': forms.TextInput(attrs={'class': 'pathChooser', 'style': "width:535px"}),
       'deployment_dir': forms.TextInput(attrs={'class': 'pathChooser', 'style': "width:535px"}),
+      'parameters': forms.widgets.HiddenInput(),
     }
     }
 
 
 
 
@@ -147,7 +148,8 @@ class CoordinatorForm(forms.ModelForm):
     model = Coordinator
     model = Coordinator
     exclude = ('owner', 'schema_version', 'deployment_dir')
     exclude = ('owner', 'schema_version', 'deployment_dir')
     widgets = {
     widgets = {
-      'description': forms.TextInput(attrs={'class': 'span5'})
+      'description': forms.TextInput(attrs={'class': 'span5'}),
+      'parameters': forms.widgets.HiddenInput(),
     }
     }
 
 
 
 

+ 189 - 63
apps/oozie/src/oozie/models.py

@@ -31,14 +31,16 @@ from django.core.validators import RegexValidator
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
 
+from desktop.log.access import access_warn
 from desktop.lib import django_mako
 from desktop.lib import django_mako
+from desktop.lib.django_util import PopupException
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
 
 
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.hadoopfs import Hdfs
 from liboozie.submittion import Submission
 from liboozie.submittion import Submission
 
 
 from oozie.management.commands import oozie_setup
 from oozie.management.commands import oozie_setup
-from oozie.conf import REMOTE_SAMPLE_DIR
+from oozie.conf import REMOTE_SAMPLE_DIR, SHARE_JOBS
 from timezones import TIMEZONES
 from timezones import TIMEZONES
 
 
 
 
@@ -50,23 +52,62 @@ name_validator = RegexValidator(regex='[a-zA-Z_][\-_a-zA-Z0-9]{1,39}',
                                 message=_('Please enter a valid value: 40 alphanum chars starting by an alpha'))
                                 message=_('Please enter a valid value: 40 alphanum chars starting by an alpha'))
 
 
 
 
+"""
+Permissions:
+
+A Workflow/Coordinator can be accessed/submitted by its owner, a superuser or by anyone if its 'is_shared'
+property and SHARE_JOBS are set to True.
+
+A Workflow/Coordinator can be modified only by its owner or a superuser.
+
+Permissions checking happens by adding the decorators.
+"""
+class JobManager(models.Manager):
+  def is_accessible_or_exception(self, request, job_id):
+    if job_id is None:
+      return
+    try:
+      job = Job.objects.select_related().get(pk=job_id).get_full_node()
+      if job.is_accessible(request.user):
+        return job
+      else:
+        message = _("Permission denied. %(username)s don't have the permissions to access job %(id)s") % \
+            {'username': request.user.username, 'id': job.id}
+        access_warn(request, message)
+        request.error(message)
+        raise PopupException(message)
+
+    except Job.DoesNotExist:
+      raise PopupException(_('job %(id)s not exist') % {'id': job_id})
+
+  def can_edit_or_exception(self, request, job):
+    if job.is_editable(request.user):
+      return True
+    else:
+      raise PopupException(_('Not allowed to modified this job'))
+
+
 class Job(models.Model):
 class Job(models.Model):
   """
   """
   Base class for Workflows and Coordinators.
   Base class for Workflows and Coordinators.
 
 
   http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/index.html
   http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/index.html
   """
   """
-  owner = models.ForeignKey(User, db_index=True)
+  owner = models.ForeignKey(User, db_index=True, help_text=_('Person who can modify the job.'))
   name = models.CharField(max_length=40, blank=False, validators=[name_validator],
   name = models.CharField(max_length=40, blank=False, validators=[name_validator],
-      help_text=_('Name of the design, which must be unique per user'))
-  description = models.CharField(max_length=1024, blank=True)
+      help_text=_('Name of the job, which must be unique per user.'))
+  description = models.CharField(max_length=1024, blank=True, help_text=_('What is the purpose of the job.'))
   last_modified = models.DateTimeField(auto_now=True, db_index=True)
   last_modified = models.DateTimeField(auto_now=True, db_index=True)
   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'))
-  is_shared = models.BooleanField(default=False, db_index=True)
+  deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_('HDFS deployment directory.'),
+                                    help_text=_('The path on the HDFS where all the workflows and '
+                                                'dependencies must be uploaded.'))
+  is_shared = models.BooleanField(default=False, db_index=True,
+                                  help_text=_('Check if you want to have some other users to have access to this job.'))
   parameters = models.TextField(default='[]',
   parameters = models.TextField(default='[]',
-                                help_text=_t('Configuration parameters (e.g. market=US)'))
+                                help_text=_t('Set some variables of the job (e.g. market=US)'))
 
 
+  objects = JobManager()
   unique_together = ('owner', 'name')
   unique_together = ('owner', 'name')
 
 
   def save(self):
   def save(self):
@@ -116,6 +157,13 @@ class Job(models.Model):
 
 
     return  [{'name': name, 'value': value} for name, value in params.iteritems()]
     return  [{'name': name, 'value': value} for name, value in params.iteritems()]
 
 
+  def is_accessible(self, user):
+    return user.is_superuser or self.owner == user or (SHARE_JOBS.get() and self.is_shared)
+
+  def is_editable(self, user):
+    """Only owners or admins can modify a job."""
+    return user.is_superuser or self.owner == user
+
 
 
 class WorkflowManager(models.Manager):
 class WorkflowManager(models.Manager):
   def new_workflow(self, owner):
   def new_workflow(self, owner):
@@ -160,7 +208,7 @@ class Workflow(Job):
   is_single = models.BooleanField(default=False)
   is_single = models.BooleanField(default=False)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
-
+  # jobxml
   objects = WorkflowManager()
   objects = WorkflowManager()
 
 
   HUE_ID = 'hue-id-w'
   HUE_ID = 'hue-id-w'
@@ -513,9 +561,10 @@ class Node(models.Model):
   """
   """
   PARAM_FIELDS = ()
   PARAM_FIELDS = ()
 
 
-  name = models.CharField(max_length=40, validators=[name_validator])
-  description = models.CharField(max_length=1024, blank=True, default='')
-  node_type = models.CharField(max_length=64, blank=False)
+  name = models.CharField(max_length=40, validators=[name_validator],
+                          help_text=_('Name of the action, it must be unique by workflow.'))
+  description = models.CharField(max_length=1024, blank=True, default='', help_text=_('What is the purpose of this action.'))
+  node_type = models.CharField(max_length=64, blank=False, help_text=_('The type of action (e.g. MapReduce, Pig...)'))
   workflow = models.ForeignKey(Workflow)
   workflow = models.ForeignKey(Workflow)
   children = models.ManyToManyField('self', related_name='parents', symmetrical=False, through=Link)
   children = models.ManyToManyField('self', related_name='parents', symmetrical=False, through=Link)
 
 
@@ -623,7 +672,7 @@ class Node(models.Model):
     copy.save()
     copy.save()
     return copy
     return copy
 
 
-  def can_edit(self):
+  def is_editable(self):
     return False
     return False
 
 
   def can_move(self):
   def can_move(self):
@@ -654,13 +703,13 @@ class Action(Node):
     # Cloning does not work anymore if not abstract
     # Cloning does not work anymore if not abstract
     abstract = True
     abstract = True
 
 
-  def can_edit(self):
+  def is_editable(self):
     return True
     return True
 
 
   def get_edit_link(self):
   def get_edit_link(self):
     return reverse('oozie:edit_action', kwargs={'action': self.id})
     return reverse('oozie:edit_action', kwargs={'action': self.id})
 
 
-
+# The fields with '[]' as default value are JSON dictionaries
 # When adding a new action, also update
 # When adding a new action, also update
 #  - Action.types below
 #  - Action.types below
 #  - Node.get_full_node()
 #  - Node.get_full_node()
@@ -670,13 +719,19 @@ class Mapreduce(Action):
   node_type = 'mapreduce'
   node_type = 'mapreduce'
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to files to be added to the distributed cache'))
+      help_text=_t('List of names or paths of files to be added to the distributed cache. '
+                   'To force a symlink for a file on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'mycat.sh#cat\'.'))
   archives = models.CharField(max_length=PATH_MAX, default="[]",
   archives = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to archives to be added to the distributed cache'))
-  job_properties = models.TextField(default='[]', # JSON dict
-                                    help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
-  jar_path = models.CharField(max_length=PATH_MAX, help_text=_t('Path to jar files on HDFS'))
-  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
+      help_text=_t('List of names or paths of the archives to be added to the distributed cache. '
+                   'To force a symlink to the uncompressed archive on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'myarch.zip#myarch\'.'))
+  job_properties = models.TextField(default='[]',
+                                    help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production)'))
+  jar_path = models.CharField(max_length=PATH_MAX,
+                              help_text=_t('Local or absolute path to the %(program)s jar file on HDFS') % {'program': 'MapReduce'})
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
+                                                         'This should be used exclusively for directory cleanup'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -695,12 +750,20 @@ class Streaming(Action):
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'mapper', 'reducer')
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'mapper', 'reducer')
   node_type = "streaming"
   node_type = "streaming"
 
 
-  files = models.CharField(max_length=PATH_MAX, default="[]")
-  archives = models.CharField(max_length=PATH_MAX, default="[]")
-  job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', # JSON dict
-                                    help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
-  mapper = models.CharField(max_length=PATH_MAX, blank=False)
-  reducer = models.CharField(max_length=PATH_MAX, blank=False)
+  files = models.CharField(max_length=PATH_MAX, default="[]",
+      help_text=_t('List of names or paths of files to be added to the distributed cache. '
+                   'To force a symlink for a file on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'mycat.sh#cat\'.'))
+  archives = models.CharField(max_length=PATH_MAX, default="[]",
+      help_text=_t('List of names or paths of the archives to be added to the distributed cache. '
+                   'To force a symlink to the uncompressed archive on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'myarch.zip#myarch\'.'))
+  job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]',
+                                    help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
+  mapper = models.CharField(max_length=PATH_MAX, blank=False,
+                            help_text=_t('The mapper element is used to specify the executable/script to be used as mapper.'))
+  reducer = models.CharField(max_length=PATH_MAX, blank=False,
+                             help_text=_t('The reducer element is used to specify the executable/script to be used as reducer.'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -718,16 +781,28 @@ class Java(Action):
   node_type = "java"
   node_type = "java"
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to files to be added to the distributed cache'))
+      help_text=_t('List of names or paths of files to be added to the distributed cache. '
+                   'To force a symlink for a file on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'mycat.sh#cat\'.'))
   archives = models.CharField(max_length=PATH_MAX, default="[]",
   archives = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to archives to be added to the distributed cache'))
-  jar_path = models.CharField(max_length=PATH_MAX, blank=False)
-  main_class = models.CharField(max_length=256, blank=False)
-  args = models.CharField(max_length=4096, blank=True)
-  java_opts = models.CharField(max_length=256, blank=True)
-  job_properties = models.TextField(default='[]', # JSON dict
-                                    help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
-  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
+      help_text=_t('List of names or paths of the archives to be added to the distributed cache. '
+                   'To force a symlink to the uncompressed archive on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'myarch.zip#myarch\'.'))
+  jar_path = models.CharField(max_length=PATH_MAX, blank=False,
+                              help_text=_t('Local or absolute path to the %(program)s jar file on HDFS') % {'program': 'Java'})
+  main_class = models.CharField(max_length=256, blank=False,
+                                help_text=_t('Full name of the Java class. e.g. org.apache.hadoop.examples.Grep'))
+  args = models.CharField(max_length=4096, blank=True,
+                          help_text=_t('Arguments of the main method. The value of each arg element is considered a single argument '
+                                       'and they are passed to the main method in the same order.'))
+  java_opts = models.CharField(max_length=256, blank=True,
+                               help_text=_t('Command line parameters which are to be used to start the JVM that will execute '
+                                            'the Java application. Using this element is equivalent to use the mapred.child.java.opts '
+                                            'configuration property'))
+  job_properties = models.TextField(default='[]',
+                                    help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
+                                                         'This should be used exclusively for directory cleanup'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -746,16 +821,21 @@ class Pig(Action):
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'params', 'prepares')
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'params', 'prepares')
   node_type = 'pig'
   node_type = 'pig'
 
 
-  script_path = models.CharField(max_length=256, blank=False, help_text=_t('Local path'))
-  params = models.TextField(default="[]", help_text=_t('The Pig parameters of the script'))
+  script_path = models.CharField(max_length=256, blank=False, help_text=_t('Local path to the Pig script. e.g. my_script.pig'))
+  params = models.TextField(default="[]", help_text=_t('The Pig parameters of the script. e.g. "-param", "INPUT=${inputDir}"'))
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to files to be added to the distributed cache'))
+      help_text=_t('List of names or paths of files to be added to the distributed cache. '
+                   'To force a symlink for a file on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'mycat.sh#cat\'.'))
   archives = models.CharField(max_length=PATH_MAX, default="[]",
   archives = models.CharField(max_length=PATH_MAX, default="[]",
-      help_text=_t('List of paths to archives to be added to the distributed cache'))
-  job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', # JSON dict
-                                    help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
-  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
+      help_text=_t('List of names or paths of the archives to be added to the distributed cache. '
+                   'To force a symlink to the uncompressed archive on the task running directory, use a \'#\' '
+                   'followed by the symlink name. For example \'myarch.zip#myarch\'.'))
+  job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]',
+                                    help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
+                                                         'This should be used exclusively for directory cleanup'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -827,7 +907,7 @@ class Fork(ControlFlow):
   def get_child_join(self):
   def get_child_join(self):
     return Link.objects.get(parent=self, name='related').child.get_full_node()
     return Link.objects.get(parent=self, name='related').child.get_full_node()
 
 
-  def can_edit(self):
+  def is_editable(self):
     return True
     return True
 
 
   def get_edit_link(self):
   def get_edit_link(self):
@@ -880,12 +960,19 @@ class Coordinator(Job):
   """
   """
   http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/CoordinatorFunctionalSpec.html
   http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/CoordinatorFunctionalSpec.html
   """
   """
-  frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS)
-  frequency_unit = models.CharField(max_length=20, choices=FREQUENCY_UNITS, default='days')
-  timezone = models.CharField(max_length=24, choices=TIMEZONES, default='America/Los_Angeles')
-  start = models.DateTimeField(default=datetime(2012, 07, 01, 0, 0))
-  end = models.DateTimeField(default=datetime(2012, 07, 01, 0, 0) + timedelta(days=3))
-  workflow = models.ForeignKey(Workflow, null=True)
+  frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS,
+                                              help_text=_t('It represents the number of units of the rate at which '
+                                                           'data is periodically created.'))
+  frequency_unit = models.CharField(max_length=20, choices=FREQUENCY_UNITS, default='days',
+                                    help_text=_t('It represents the unit of the rate at which data is periodically created.'))
+  timezone = models.CharField(max_length=24, choices=TIMEZONES, default='America/Los_Angeles',
+                              help_text=_t('The timezone of the Coordinator.'))
+  start = models.DateTimeField(default=datetime.today(),
+                               help_text=_t('When we need to start the first workflow.'))
+  end = models.DateTimeField(default=datetime.today() + timedelta(days=3),
+                             help_text=_t('When we need to start the last workflow.'))
+  workflow = models.ForeignKey(Workflow, null=True,
+                               help_text=_t('The corresponding workflow we want to schedule repeatedly.'))
 
 
   HUE_ID = 'hue-id-w'
   HUE_ID = 'hue-id-w'
 
 
@@ -978,17 +1065,52 @@ def utc_datetime_format(utc_time):
   return utc_time.strftime("%Y-%m-%dT%H:%MZ")
   return utc_time.strftime("%Y-%m-%dT%H:%MZ")
 
 
 
 
-class Dataset(models.Model):
-  name = models.CharField(max_length=40, validators=[name_validator])
-  description = models.CharField(max_length=1024, blank=True, default='')
-  start = models.DateTimeField(default=datetime.today())
-  frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS)
-  frequency_unit = models.CharField(max_length=20, choices=FREQUENCY_UNITS, default='days')
-  uri = models.CharField(max_length=1024, default='/data/${YEAR}${MONTH}${DAY}')
-  timezone = models.CharField(max_length=24, choices=TIMEZONES, default='America/Los_Angeles')
-  done_flag = models.CharField(max_length=64, blank=True, default='')
-  coordinator = models.ForeignKey(Coordinator)
+class DatasetManager(models.Manager):
+  def is_accessible_or_exception(self, request, dataset_id):
+    if dataset_id is None:
+      return
+    try:
+      dataset = Dataset.objects.get(pk=dataset_id)
+      if dataset.coordinator.is_accessible(request.user):
+        return dataset
+      else:
+        message = _("Permission denied. %(username)s don't have the permissions to access dataset %(id)s") % \
+            {'username': request.user.username, 'id': dataset.id}
+        access_warn(request, message)
+        request.error(message)
+        raise PopupException(message)
+
+    except Dataset.DoesNotExist:
+      raise PopupException(_('dataset %(id)s not exist') % {'id': dataset_id})
 
 
+
+class Dataset(models.Model):
+  name = models.CharField(max_length=40, validators=[name_validator],
+                          help_text=_t('The name of the dataset.)'))
+  description = models.CharField(max_length=1024, blank=True, default='',
+                                 help_text=_t('More details about the dataset.'))
+  start = models.DateTimeField(default=datetime.today(),
+                               help_text=_t(' The UTC datetime of the initial instance of the dataset. The initial-instance also provides '
+                                            'the baseline datetime to compute instances of the dataset using multiples of the frequency.'))
+  frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS,
+                                              help_text=_t('It represents the number of units of the rate at which '
+                                                           'data is periodically created.'))
+  frequency_unit = models.CharField(max_length=20, choices=FREQUENCY_UNITS, default='days',
+                                    help_text=_t('It represents the unit of the rate at which data is periodically created.'))
+  uri = models.CharField(max_length=1024, default='/data/${YEAR}${MONTH}${DAY}',
+                         help_text=_t('The URI template that identifies the dataset and can be resolved into concrete URIs to identify a particular '
+                                      'dataset instance. The URI consist of constants (e.g. ${YEAR}/${MONTH}) and '
+                                      'configuration properties (e.g. Ex: ${YEAR}/${MONTH})'))
+  timezone = models.CharField(max_length=24, choices=TIMEZONES, default='America/Los_Angeles',
+                              help_text=_t('The timezone of the dataset.'))
+  done_flag = models.CharField(max_length=64, blank=True, default='',
+                               help_text=_t(' The done file for the data set. If done-flag is not specified, then Oozie '
+                                            'configures Hadoop to create a _SUCCESS file in the output directory. If the done '
+                                            'flag is set to empty, then Coordinator looks for the existence of the directory itself.'))
+  coordinator = models.ForeignKey(Coordinator,
+                                  help_text=_t('The coordinator associated with this data.'))
+
+  objects = DatasetManager()
   unique_together = ('coordinator', 'name')
   unique_together = ('coordinator', 'name')
 
 
   def __unicode__(self):
   def __unicode__(self):
@@ -1008,16 +1130,20 @@ class Dataset(models.Model):
 
 
 
 
 class DataInput(models.Model):
 class DataInput(models.Model):
-  name = models.CharField(max_length=40, validators=[name_validator], verbose_name=_('Name of an input variable in the workflow'))
-  dataset = models.OneToOneField(Dataset, verbose_name=_('Pick the dataset representing format of the data input'))
+  name = models.CharField(max_length=40, validators=[name_validator], verbose_name=_('Name of an input variable in the workflow'),
+                          help_text=_t('The name of the variable of the workflow to automatically filled up.'))
+  dataset = models.OneToOneField(Dataset, verbose_name=_('Pick the dataset representing format of the data input'),
+                                 help_text=_t('The pattern of the input data we want to process.'))
   coordinator = models.ForeignKey(Coordinator)
   coordinator = models.ForeignKey(Coordinator)
 
 
   unique_together = ('coordinator', 'name')
   unique_together = ('coordinator', 'name')
 
 
 
 
 class DataOutput(models.Model):
 class DataOutput(models.Model):
-  name = models.CharField(max_length=40, validators=[name_validator], verbose_name=_('Name of an output variable in the workflow'))
-  dataset = models.OneToOneField(Dataset, verbose_name=_('Pick the dataset representing the format of the data output'))
+  name = models.CharField(max_length=40, validators=[name_validator], verbose_name=_('Name of an output variable in the workflow'),
+                          help_text=_t('The name of the variable of the workflow to automatically filled up.'))
+  dataset = models.OneToOneField(Dataset, verbose_name=_('Pick the dataset representing the format of the data output'),
+                                 help_text=_t('The pattern of the output data we want to generate.'))
   coordinator = models.ForeignKey(Coordinator)
   coordinator = models.ForeignKey(Coordinator)
 
 
   unique_together = ('coordinator', 'name')
   unique_together = ('coordinator', 'name')

+ 1 - 0
apps/oozie/src/oozie/templates/editor/create_coordinator.mako

@@ -50,6 +50,7 @@ ${ layout.menubar(section='coordinators') }
              ${ utils.render_field(coordinator_form['name']) }
              ${ utils.render_field(coordinator_form['name']) }
              ${ utils.render_field(coordinator_form['description']) }
              ${ utils.render_field(coordinator_form['description']) }
              ${ utils.render_field(coordinator_form['workflow']) }
              ${ utils.render_field(coordinator_form['workflow']) }
+             ${ coordinator_form['parameters'] }
            </div>
            </div>
 
 
           <hr/>
           <hr/>

+ 8 - 8
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']) }

+ 14 - 5
apps/oozie/src/oozie/templates/editor/edit_coordinator.mako

@@ -213,7 +213,7 @@ ${ layout.menubar(section='coordinators') }
             % endif
             % endif
           </div>
           </div>
 
 
-          <div class="span10">
+          <div class="span9">
             % if coordinator.id:
             % if coordinator.id:
               <div>
               <div>
                 <table class="table table-striped table-condensed" cellpadding="0" cellspacing="0">
                 <table class="table table-striped table-condensed" cellpadding="0" cellspacing="0">
@@ -286,11 +286,16 @@ ${ layout.menubar(section='coordinators') }
           </thead>
           </thead>
           <tbody>
           <tbody>
             % if not history:
             % if not history:
-              ${ _('N/A') }
+              <tr>
+                <td>${ _('N/A') }</td><td></td>
+              </tr>
             % endif
             % endif
             % for record in history:
             % for record in history:
                 <tr>
                 <tr>
-                  <td><a href="${ url('oozie:list_history_record', record_id=record.id) }" data-row-selector="true"></a>${ record.submission_date }</td>
+                  <td>
+                    <a href="${ url('oozie:list_history_record', record_id=record.id) }" data-row-selector="true"></a>
+                    ${ utils.format_date(record.submission_date) }
+                  </td>
                   <td>${ record.oozie_job_id }</td>
                   <td>${ record.oozie_job_id }</td>
                 </tr>
                 </tr>
             % endfor
             % endfor
@@ -303,7 +308,7 @@ ${ layout.menubar(section='coordinators') }
   </div>
   </div>
 
 
   <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_coordinators') }" class="btn">${ _('Back') }</a>
     % if can_edit_coordinator:
     % if can_edit_coordinator:
       <input class="btn btn-primary" data-bind="click: submit" type="submit" value="${ _('Save') }"></input>
       <input class="btn btn-primary" data-bind="click: submit" type="submit" value="${ _('Save') }"></input>
     % endif
     % endif
@@ -390,9 +395,13 @@ ${ layout.menubar(section='coordinators') }
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
 
 
     ko.applyBindings(window.viewModel);
     ko.applyBindings(window.viewModel);
+
+    $("*[rel=popover]").popover({
+      placement: 'right'
+    });
  });
  });
 </script>
 </script>
 
 
 % endif
 % endif
 
 
-${commonfooter(messages)}
+${ commonfooter(messages) }

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

@@ -128,8 +128,8 @@ ${ layout.menubar(section='workflows') }
 
 
       <div class="tab-pane" id="properties">
       <div class="tab-pane" id="properties">
         <div class="row-fluid">
         <div class="row-fluid">
-          <div class="span2"></div>
-          <div class="span10">
+          <div class="span1"></div>
+          <div class="span8">
             <h2>${ _('Properties') }</h2>
             <h2>${ _('Properties') }</h2>
             <br/>
             <br/>
               <fieldset>
               <fieldset>
@@ -147,6 +147,7 @@ ${ layout.menubar(section='workflows') }
             <button data-bind="click: submit" class="btn btn-primary">${ _('Save') }</button>
             <button data-bind="click: submit" class="btn btn-primary">${ _('Save') }</button>
           % endif
           % endif
         </div>
         </div>
+        <div class="span3"></div>
       </div>
       </div>
 
 
       <div class="tab-pane" id="history">
       <div class="tab-pane" id="history">
@@ -163,7 +164,10 @@ ${ layout.menubar(section='workflows') }
           <tbody>
           <tbody>
             % for record in history:
             % for record in history:
               <tr>
               <tr>
-                <td><a href="${ url('oozie:list_history_record', record_id=record.id) }" data-row-selector="true"></a>${ record.submission_date }</td>
+                <td>
+                  <a href="${ url('oozie:list_history_record', record_id=record.id) }" data-row-selector="true"></a>
+                  ${ utils.format_date(record.submission_date) }
+                </td>
                 <td>${ record.oozie_job_id }</td>
                 <td>${ record.oozie_job_id }</td>
               </tr>
               </tr>
             % endfor
             % endfor
@@ -213,6 +217,10 @@ ${ layout.menubar(section='workflows') }
     ko.applyBindings(window.viewModel);
     ko.applyBindings(window.viewModel);
 
 
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
+
+    $("*[rel=popover]").popover({
+      placement: 'right'
+    });
   });
   });
 </script>
 </script>
 
 

+ 169 - 155
apps/oozie/src/oozie/templates/editor/edit_workflow_action.mako

@@ -34,199 +34,209 @@ ${ layout.menubar(section='workflows') }
   </h1>
   </h1>
 
 
   <br/>
   <br/>
-  <form class="form-horizontal" id="actionForm" action="${ form_url }" method="POST">
-    <fieldset>
-    % for field in action_form:
-      % if field.html_name in ('name', 'description'):
-        ${ utils.render_field(field) }
-      % endif
-    % endfor
 
 
-    ${ utils.render_constant(_('Action type'), node_type) }
+  <div class="row">
+    <div class="span12">
+    <form class="form-horizontal" id="actionForm" action="${ form_url }" method="POST">
+      <fieldset>
+      % for field in action_form:
+        % if field.html_name in ('name', 'description'):
+          ${ utils.render_field(field) }
+        % endif
+      % endfor
+
+      ${ utils.render_constant(_('Action type'), node_type) }
 
 
-    <hr/>
+      <hr/>
 
 
-    <div class="control-group">
-      <label class="control-label"></label>
-      <div class="controls">
-      <p class="alert alert-info span5">
-        ${ _('You can parameterize the values using uppercase') } <code>${"${"}VAR}</code>.
-      </p>
+      <div class="control-group">
+        <label class="control-label"></label>
+        <div class="controls">
+        <p class="alert alert-info span5">
+          ${ _('You can parameterize the values using uppercase') } <code>${"${"}VAR}</code>.
+        </p>
+        </div>
       </div>
       </div>
-    </div>
 
 
-    % for field in action_form:
-      % if field.html_name not in ('name', 'description', 'node_type'):
-        ${ utils.render_field(field) }
+      % for field in action_form:
+        % if field.html_name not in ('name', 'description', 'node_type'):
+          ${ utils.render_field(field) }
+        % endif
+      % endfor
+
+      % if 'prepares' in action_form.fields:
+        <div class="control-group" rel="popover"
+            data-original-title="${ action_form['prepares'].label }" data-content="${ action_form['prepares'].help_text }">
+          <label class="control-label">${ _('Prepare') }</label>
+          <div class="controls">
+            <table class="table-condensed designTable" data-bind="visible: prepares().length > 0">
+              <thead>
+                <tr>
+                  <th>${ _('Type') }</th>
+                  <th>${ _('Value') }</th>
+                  <th/>
+                </tr>
+              </thead>
+              <tbody data-bind="foreach: prepares">
+                <tr>
+                  <td>
+                    <span class="span3 required" data-bind="text: type" />
+                  </td>
+                  <td>
+                    <input class="input span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" />
+                  </td>
+                  <td><a class="btn" href="#" data-bind="click: $root.removePrepare">${ _('Delete') }</a></td>
+                </tr>
+              </tbody>
+            </table>
+
+            % if len(action_form['prepares'].errors):
+              <div class="alert alert-error">
+                ${ unicode(action_form['prepares'].errors) | n }
+              </div>
+            % endif
+
+            <button class="btn" data-bind="click: addPrepareDelete">${ _('Add delete') }</button>
+            <button class="btn" data-bind="click: addPrepareMkdir">${ _('Add mkdir') }</button>
+          </div>
+        </div>
       % endif
       % endif
-    % endfor
 
 
-    % if 'prepares' in action_form.fields:
-      <div class="control-group">
-        <label class="control-label">${ _('Prepare') }</label>
+      % if 'params' in action_form.fields:
+        <div class="control-group" rel="popover"
+            data-original-title="${ action_form['params'].label }" data-content="${ action_form['params'].help_text }">
+          <label class="control-label">${ _('Params') }</label>
+          <div class="controls">
+            <table class="table-condensed designTable" data-bind="visible: params().length > 0">
+              <thead>
+                <tr>
+                  <th>${ _('Type') }</th>
+                  <th>${ _('Value') }</th>
+                  <th/>
+                </tr>
+              </thead>
+              <tbody data-bind="foreach: params">
+                <tr>
+                  <td>
+                    <span class="span3 required" data-bind="text: type" />
+                  </td>
+                  <td>
+                    <input class="input span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" />
+                  </td>
+                  <td><a class="btn" href="#" data-bind="click: $root.removeParam">${ _('Delete') }</a></td>
+                </tr>
+              </tbody>
+            </table>
+
+            % if len(action_form['params'].errors):
+              <div class="alert alert-error">
+                ${ unicode(action_form['params'].errors) | n }
+              </div>
+            % endif
+
+            <button class="btn" data-bind="click: addParam">${ _('Add Param') }</button>
+            <button class="btn" data-bind="click: addArgument">${ _('Add Argument') }</button>
+          </div>
+        </div>
+      % endif
+
+      % if 'job_properties' in action_form.fields:
+      <div class="control-group" rel="popover"
+          data-original-title="${ action_form['job_properties'].label }" data-content="${ action_form['job_properties'].help_text }">
+        <label class="control-label">${ _('Job Properties') }</label>
         <div class="controls">
         <div class="controls">
-          <table class="table-condensed designTable" data-bind="visible: prepares().length > 0">
+          <table class="table-condensed designTable" data-bind="visible: properties().length > 0">
             <thead>
             <thead>
               <tr>
               <tr>
-                <th>${ _('Type') }</th>
+                <th>${ _('Property name') }</th>
                 <th>${ _('Value') }</th>
                 <th>${ _('Value') }</th>
                 <th/>
                 <th/>
               </tr>
               </tr>
             </thead>
             </thead>
-            <tbody data-bind="foreach: prepares">
+            <tbody data-bind="foreach: properties">
               <tr>
               <tr>
-                <td>
-                  <span class="span3 required" data-bind="text: type" />
-                </td>
-                <td>
-                  <input class="input span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" />
-                </td>
-                <td><a class="btn" href="#" data-bind="click: $root.removePrepare">${ _('Delete') }</a></td>
+                <td><input class="span4 required propKey" data-bind="value: name, uniqueName: false" /></td>
+                <td><input class="span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" /></td>
+                <td><a class="btn btn-small" href="#" data-bind="click: $root.removeProp">${ _('Delete') }</a></td>
               </tr>
               </tr>
             </tbody>
             </tbody>
           </table>
           </table>
-
-          % if len(action_form['prepares'].errors):
-            <div class="alert alert-error">
-              ${ unicode(action_form['prepares'].errors) | n }
+          % if len(action_form['job_properties'].errors):
+            <div class="row">
+              <div class="alert alert-error">
+                ${ unicode(action_form['job_properties'].errors) | n }
+              </div>
             </div>
             </div>
           % endif
           % endif
 
 
-          <button class="btn" data-bind="click: addPrepareDelete">${ _('Add delete') }</button>
-          <button class="btn" data-bind="click: addPrepareMkdir">${ _('Add mkdir') }</button>
+          <button class="btn" data-bind="click: addProp">${ _('Add Property') }</button>
         </div>
         </div>
       </div>
       </div>
-    % endif
+      % endif
 
 
-    % if 'params' in action_form.fields:
-      <div class="control-group">
-        <label class="control-label">${ _('Params') }</label>
+      % if 'files' in action_form.fields:
+      <div class="control-group" rel="popover"
+        data-original-title="${ action_form['files'].label }" data-content="${ action_form['files'].help_text }">
+          <label class="control-label">${ _('Files') }</label>
+          <div class="controls">
+              <table class="table-condensed designTable" data-bind="visible: files().length > 0">
+                <tbody data-bind="foreach: files">
+                  <tr>
+                    <td><input class="input span5 required pathChooserKo"
+                            data-bind="fileChooser: $data, value: name, uniqueName: false" />
+                    </td>
+                    <td><a class="btn" href="#" data-bind="click: $root.removeFile">${ _('Delete') }</a></td>
+                  </tr>
+                </tbody>
+              </table>
+              % if len(action_form['files'].errors):
+                <div class="alert alert-error">
+                  ${ unicode(action_form['files'].errors) | n }
+                </div>
+              % endif
+
+              <button class="btn" data-bind="click: addFile">${ _('Add File') }</button>
+          </div>
+      </div>
+      % endif
+
+      % if 'archives' in action_form.fields:
+      <div class="control-group" rel="popover"
+          data-original-title="${ action_form['archives'].label }" data-content="${ action_form['archives'].help_text }">
+        <label class="control-label">${ _('Archives') }</label>
         <div class="controls">
         <div class="controls">
-          <table class="table-condensed designTable" data-bind="visible: params().length > 0">
-            <thead>
-              <tr>
-                <th>${ _('Type') }</th>
-                <th>${ _('Value') }</th>
-                <th/>
-              </tr>
-            </thead>
-            <tbody data-bind="foreach: params">
+          <table class="table-condensed designTable" data-bind="visible: archives().length > 0">
+            <tbody data-bind="foreach: archives">
               <tr>
               <tr>
                 <td>
                 <td>
-                  <span class="span3 required" data-bind="text: type" />
+                  <input class="input span5 required pathChooserKo"
+                      data-bind="fileChooser: $data, value: name, uniqueName: false" />
                 </td>
                 </td>
-                <td>
-                  <input class="input span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" />
-                </td>
-                <td><a class="btn" href="#" data-bind="click: $root.removeParam">${ _('Delete') }</a></td>
+                <td><a class="btn" href="#" data-bind="click: $root.removeArchive">${ _('Delete') }</a></td>
               </tr>
               </tr>
             </tbody>
             </tbody>
           </table>
           </table>
-
-          % if len(action_form['params'].errors):
+          % if len(action_form['archives'].errors):
             <div class="alert alert-error">
             <div class="alert alert-error">
-              ${ unicode(action_form['params'].errors) | n }
+              ${ unicode(action_form['archives'].errors) | n }
             </div>
             </div>
           % endif
           % endif
 
 
-          <button class="btn" data-bind="click: addParam">${ _('Add Param') }</button>
-          <button class="btn" data-bind="click: addArgument">${ _('Add Argument') }</button>
-        </div>
+          <button class="btn" data-bind="click: addArchive">${ _('Add Archive') }</button>
+         </div>
       </div>
       </div>
-    % endif
-
-    % if 'job_properties' in action_form.fields:
-    <div class="control-group">
-      <label class="control-label">${ _('Job Properties') }</label>
-      <div class="controls">
-        <table class="table-condensed designTable" data-bind="visible: properties().length > 0">
-          <thead>
-            <tr>
-              <th>${ _('Property name') }</th>
-              <th>${ _('Value') }</th>
-              <th/>
-            </tr>
-          </thead>
-          <tbody data-bind="foreach: properties">
-            <tr>
-              <td><input class="span4 required propKey" data-bind="value: name, uniqueName: false" /></td>
-              <td><input class="span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" /></td>
-              <td><a class="btn btn-small" href="#" data-bind="click: $root.removeProp">${ _('Delete') }</a></td>
-            </tr>
-          </tbody>
-        </table>
-        % if len(action_form['job_properties'].errors):
-          <div class="row">
-            <div class="alert alert-error">
-              ${ unicode(action_form['job_properties'].errors) | n }
-            </div>
-          </div>
-        % endif
-
-        <button class="btn" data-bind="click: addProp">${ _('Add Property') }</button>
-      </div>
-    </div>
-    % endif
-
-    % if 'files' in action_form.fields:
-    <div class="control-group">
-        <label class="control-label">${ _('Files') }</label>
-        <div class="controls">
-            <table class="table-condensed designTable" data-bind="visible: files().length > 0">
-              <tbody data-bind="foreach: files">
-                <tr>
-                  <td><input class="input span5 required pathChooserKo"
-                          data-bind="fileChooser: $data, value: name, uniqueName: false" />
-                  </td>
-                  <td><a class="btn" href="#" data-bind="click: $root.removeFile">${ _('Delete') }</a></td>
-                </tr>
-              </tbody>
-            </table>
-            % if len(action_form['files'].errors):
-              <div class="alert alert-error">
-                ${ unicode(action_form['files'].errors) | n }
-              </div>
-            % endif
+      % endif
+      </fieldset>
 
 
-            <button class="btn" data-bind="click: addFile">${ _('Add File') }</button>
-        </div>
-    </div>
-    % endif
-
-    % if 'archives' in action_form.fields:
-    <div class="control-group">
-      <label class="control-label">${ _('Archives') }</label>
-      <div class="controls">
-        <table class="table-condensed designTable" data-bind="visible: archives().length > 0">
-          <tbody data-bind="foreach: archives">
-            <tr>
-              <td>
-                <input class="input span5 required pathChooserKo"
-                    data-bind="fileChooser: $data, value: name, uniqueName: false" />
-              </td>
-              <td><a class="btn" href="#" data-bind="click: $root.removeArchive">${ _('Delete') }</a></td>
-            </tr>
-          </tbody>
-        </table>
-        % if len(action_form['archives'].errors):
-          <div class="alert alert-error">
-            ${ unicode(action_form['archives'].errors) | n }
-          </div>
+      <div class="form-actions">
+        <a href="${ url('oozie:edit_workflow', workflow=workflow.id) }" class="btn">${ _('Cancel') }</a>
+        % if can_edit_action:
+          <button data-bind="click: submit" class="btn btn-primary">${ _('Save') }</button>
         % endif
         % endif
-
-        <button class="btn" data-bind="click: addArchive">${ _('Add Archive') }</button>
-       </div>
-    </div>
-    % endif
-    </fieldset>
-
-    <div class="form-actions">
-      <a href="${ url('oozie:edit_workflow', workflow=workflow.id) }" class="btn">${ _('Cancel') }</a>
-      % if can_edit_action:
-        <button data-bind="click: submit" class="btn btn-primary">${ _('Save') }</button>
-      % endif
-    </div>
-  </form>
+      </div>
+    </form>
+  </div>
+  <div class="span1"></div>
 </div>
 </div>
 
 
 
 
@@ -437,6 +447,10 @@ ${ layout.menubar(section='workflows') }
     }
     }
 
 
     $(".propKey").each(addAutoComplete);
     $(".propKey").each(addAutoComplete);
+
+    $("*[rel=popover]").popover({
+      placement: 'right'
+    });
   });
   });
 </script>
 </script>
 
 

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

@@ -37,7 +37,7 @@
           ${ hidden }
           ${ hidden }
         % endfor
         % endfor
         <div class="row-fluid">
         <div class="row-fluid">
-            % if node.can_edit():
+            % if node.is_editable():
             <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('Edit') }">
             <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('Edit') }">
           % else:
           % else:
             <div class="span10">
             <div class="span10">
@@ -47,7 +47,7 @@
             <div class="span2"></div>
             <div class="span2"></div>
         </div>
         </div>
         <div class="row-fluid">
         <div class="row-fluid">
-          % if node.can_edit():
+          % if node.is_editable():
             <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('Edit') }">
             <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('Edit') }">
           % else:
           % else:
             <div class="span10">
             <div class="span10">

+ 2 - 2
apps/oozie/src/oozie/templates/editor/gen/workflow-graph-readonly.xml.mako

@@ -37,7 +37,7 @@ from django.utils.translation import ugettext as _
         ${ hidden }
         ${ hidden }
       % endfor
       % endfor
       <div class="row-fluid">
       <div class="row-fluid">
-          % if node.can_edit():
+          % if node.is_editable():
           <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('View') }">
           <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('View') }">
         % else:
         % else:
           <div class="span10">
           <div class="span10">
@@ -47,7 +47,7 @@ from django.utils.translation import ugettext as _
           <div class="span2"></div>
           <div class="span2"></div>
       </div>
       </div>
       <div class="row-fluid">
       <div class="row-fluid">
-        % if node.can_edit():
+        % if node.is_editable():
           <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('View') }">
           <div class="span10 action-link" data-edit="${ node.get_edit_link() }" title="${ _('View') }">
         % else:
         % else:
           <div class="span10">
           <div class="span10">

+ 3 - 2
apps/oozie/src/oozie/templates/editor/job_action_properties.mako

@@ -22,7 +22,8 @@
 
 
 
 
 <%def name="print_key_value(label, element, form, initial_parameters)">
 <%def name="print_key_value(label, element, form, initial_parameters)">
-  <div class="control-group ko-${element}">
+  <div class="control-group ko-${element}" rel="popover"
+      data-original-title="${ label }" data-content="${ _('Set some variables of the job (e.g. market=US)') }">
     <label class="control-label">${ label }</label>
     <label class="control-label">${ label }</label>
     <div class="controls">
     <div class="controls">
       <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
       <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
@@ -41,7 +42,7 @@
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>
-      % if len(form[element].errors):
+      % if form[element].errors:
         <div class="row">
         <div class="row">
           <div class="alert alert-error">
           <div class="alert alert-error">
             ${ unicode(form[element].errors) | n }
             ${ unicode(form[element].errors) | n }

+ 3 - 5
apps/oozie/src/oozie/templates/editor/list_coordinators.mako

@@ -17,8 +17,6 @@
 <%!
 <%!
   from desktop.views import commonheader, commonfooter
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
   from django.utils.translation import ugettext as _
-
-  from oozie.views import can_access_job, can_edit_job
 %>
 %>
 
 
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="layout" file="../navigation-bar.mako" />
@@ -72,17 +70,17 @@ ${ layout.menubar(section='coordinators') }
         <tr class="action-row">
         <tr class="action-row">
           <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 coordinator.is_editable(currentuser):
                   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 coordinator.is_accessible(currentuser):
                   data-clone-url="${ url('oozie:clone_coordinator', coordinator=coordinator.id) }"
                   data-clone-url="${ url('oozie:clone_coordinator', coordinator=coordinator.id) }"
                   data-bundle-url="${ url('oozie:create_coordinator') }"
                   data-bundle-url="${ url('oozie:create_coordinator') }"
                   data-submit-url="${ url('oozie:submit_coordinator', coordinator=coordinator.id) }"
                   data-submit-url="${ url('oozie:submit_coordinator', coordinator=coordinator.id) }"
               % endif
               % endif
               >
               >
             </input>
             </input>
-            % if can_access_job(currentuser, coordinator):
+            % if coordinator.is_accessible(currentuser):
               <a href="${ url('oozie:edit_coordinator', coordinator=coordinator.id) }" data-row-selector="true"/>
               <a href="${ url('oozie:edit_coordinator', coordinator=coordinator.id) }" data-row-selector="true"/>
             % endif
             % endif
           </td>
           </td>

+ 4 - 6
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -17,8 +17,6 @@
 <%!
 <%!
   from desktop.views import commonheader, commonfooter
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
   from django.utils.translation import ugettext as _
-
-  from oozie.views import can_access_job, can_edit_job
 %>
 %>
 
 
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="layout" file="../navigation-bar.mako" />
@@ -75,16 +73,16 @@ ${ layout.menubar(section='workflows') }
         <tr class="action-row">
         <tr class="action-row">
           <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 workflow.is_accessible(currentuser):
                   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:schedule_workflow', workflow=workflow.id) }"
               % endif
               % endif
-              % if can_edit_job(currentuser, workflow):
+              % if workflow.is_editable(currentuser):
                   data-delete-url="${ url('oozie:delete_workflow', workflow=workflow.id) }"
                   data-delete-url="${ url('oozie:delete_workflow', workflow=workflow.id) }"
                   data-clone-url="${ url('oozie:clone_workflow', workflow=workflow.id) }"
                   data-clone-url="${ url('oozie:clone_workflow', workflow=workflow.id) }"
               % endif
               % endif
             />
             />
-            % if can_access_job(currentuser, workflow):
+            % if workflow.is_accessible(currentuser):
               <a href="${ url('oozie:edit_workflow', workflow=workflow.id) }" data-row-selector="true"></a>
               <a href="${ url('oozie:edit_workflow', workflow=workflow.id) }" data-row-selector="true"></a>
             % endif
             % endif
           </td>
           </td>

+ 1 - 1
apps/oozie/src/oozie/templates/navigation-bar.mako

@@ -27,7 +27,7 @@
       <ul class="nav nav-pills">
       <ul class="nav nav-pills">
         <li class="${utils.is_selected(section, 'dashboard')}"><a href="${url('oozie:list_oozie_workflows')}">${ _('Dashboard') }</a></li>
         <li class="${utils.is_selected(section, 'dashboard')}"><a href="${url('oozie:list_oozie_workflows')}">${ _('Dashboard') }</a></li>
         <li class="${utils.is_selected(section, 'workflows')}"><a href="${url('oozie:list_workflows')}">${ _('Workflows') }</a></li>
         <li class="${utils.is_selected(section, 'workflows')}"><a href="${url('oozie:list_workflows')}">${ _('Workflows') }</a></li>
-        <li class="${utils.is_selected(section, 'coordinators')}"><a href="${url('oozie:list_coordinator')}">${ _('Coordinators') }</a></li>
+        <li class="${utils.is_selected(section, 'coordinators')}"><a href="${url('oozie:list_coordinators')}">${ _('Coordinators') }</a></li>
         <li class="${utils.is_selected(section, 'history')}"><a href="${url('oozie:list_history')}">${ _('History') }</a></li>
         <li class="${utils.is_selected(section, 'history')}"><a href="${url('oozie:list_history')}">${ _('History') }</a></li>
       </ul>
       </ul>
     </div>
     </div>

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

@@ -138,15 +138,16 @@
 
 
 
 
 <%def name="render_field(field, show_label=True)">
 <%def name="render_field(field, show_label=True)">
-  %if not field.is_hidden:
-    <% group_class = len(field.errors) and "error" or "" %>
-    <div class="control-group ${group_class}">
+  % if not field.is_hidden:
+    <% group_class = field.errors and "error" or "" %>
+    <div class="control-group ${group_class}"
+      rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
       % if show_label:
       % if show_label:
         <label class="control-label">${ field.label | n }</label>
         <label class="control-label">${ field.label | n }</label>
       % endif
       % endif
       <div class="controls">
       <div class="controls">
         ${ field }
         ${ field }
-        % if len(field.errors):
+        % if field.errors:
           <span class="help-inline">${ unicode(field.errors) | n }</span>
           <span class="help-inline">${ unicode(field.errors) | n }</span>
         % endif
         % endif
       </div>
       </div>

+ 3 - 3
apps/oozie/src/oozie/tests.py

@@ -668,13 +668,13 @@ class TestEditor:
     # List
     # List
     finish = SHARE_JOBS.set_for_testing(True)
     finish = SHARE_JOBS.set_for_testing(True)
     try:
     try:
-      response = client_not_me.get(reverse('oozie:list_coordinator'))
+      response = client_not_me.get(reverse('oozie:list_coordinators'))
       assert_false('MyCoord' in response.content, response.content)
       assert_false('MyCoord' in response.content, response.content)
     finally:
     finally:
       finish()
       finish()
     finish = SHARE_JOBS.set_for_testing(False)
     finish = SHARE_JOBS.set_for_testing(False)
     try:
     try:
-      response = client_not_me.get(reverse('oozie:list_coordinator'))
+      response = client_not_me.get(reverse('oozie:list_coordinators'))
       assert_false('MyCoord' in response.content, response.content)
       assert_false('MyCoord' in response.content, response.content)
     finally:
     finally:
       finish()
       finish()
@@ -702,7 +702,7 @@ class TestEditor:
     # List
     # List
     finish = SHARE_JOBS.set_for_testing(True)
     finish = SHARE_JOBS.set_for_testing(True)
     try:
     try:
-      response = client_not_me.get(reverse('oozie:list_coordinator'))
+      response = client_not_me.get(reverse('oozie:list_coordinators'))
       assert_equal(200, response.status_code)
       assert_equal(200, response.status_code)
       assert_true('MyCoord' in response.content, response.content)
       assert_true('MyCoord' in response.content, response.content)
     finally:
     finally:

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

@@ -30,6 +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'^schedule_workflow/(?P<workflow>\d+)$', 'schedule_workflow', name='schedule_workflow'),
   url(r'^resubmit_workflow/(?P<oozie_wf_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'),
@@ -41,7 +42,7 @@ urlpatterns = patterns(
   url(r'^move_up_action/(?P<action>\d+)$', 'move_up_action', name='move_up_action'),
   url(r'^move_up_action/(?P<action>\d+)$', 'move_up_action', name='move_up_action'),
   url(r'^move_down_action/(?P<action>\d+)$', 'move_down_action', name='move_down_action'),
   url(r'^move_down_action/(?P<action>\d+)$', 'move_down_action', name='move_down_action'),
 
 
-  url(r'^list_coordinator/$', 'list_workflows', name='list_coordinator', kwargs={'job_type': 'coordinators'}),
+  url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
   url(r'^create_coordinator/(?P<workflow>[-\w]+)?$', 'create_coordinator', name='create_coordinator'),
   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'^edit_coordinator/(?P<coordinator>[-\w]+)$', 'edit_coordinator', name='edit_coordinator'),
   url(r'^delete_coordinator/(?P<coordinator>\d+)$', 'delete_coordinator', name='delete_coordinator'),
   url(r'^delete_coordinator/(?P<coordinator>\d+)$', 'delete_coordinator', name='delete_coordinator'),

+ 3 - 4
apps/oozie/src/oozie/views/dashboard.py

@@ -31,8 +31,7 @@ from desktop.log.access import access_warn
 from liboozie.oozie_api import get_oozie
 from liboozie.oozie_api import get_oozie
 
 
 from oozie.conf import OOZIE_JOBS_COUNT
 from oozie.conf import OOZIE_JOBS_COUNT
-from oozie.models import History
-from oozie.views.editor import can_access_job_or_exception
+from oozie.models import History, Job
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -113,8 +112,8 @@ def list_oozie_workflow(request, job_id, coordinator_job_id=None):
   hue_coord = history and history.get_coordinator() or History.get_coordinator_from_config(oozie_workflow.conf_dict)
   hue_coord = history and history.get_coordinator() or History.get_coordinator_from_config(oozie_workflow.conf_dict)
   hue_workflow = (hue_coord and hue_coord.workflow) or (history and history.get_workflow()) or History.get_workflow_from_config(oozie_workflow.conf_dict)
   hue_workflow = (hue_coord and hue_coord.workflow) or (history and history.get_workflow()) or History.get_workflow_from_config(oozie_workflow.conf_dict)
 
 
-  if hue_coord: can_access_job_or_exception(request, hue_coord.workflow.id)
-  if hue_workflow: can_access_job_or_exception(request, hue_workflow.id)
+  if hue_coord: Job.objects.is_accessible_or_exception(request, hue_coord.workflow.id)
+  if hue_workflow: Job.objects.is_accessible_or_exception(request, hue_workflow.id)
 
 
   # Add parameters from coordinator to workflow if possible
   # Add parameters from coordinator to workflow if possible
   parameters = {}
   parameters = {}

+ 61 - 101
apps/oozie/src/oozie/views/editor.py

@@ -20,7 +20,6 @@ try:
 except ImportError:
 except ImportError:
   import simplejson as json
   import simplejson as json
 import logging
 import logging
-import re
 
 
 
 
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
@@ -35,17 +34,15 @@ from django.utils.translation import ugettext as _
 
 
 from desktop.lib.django_util import render, PopupException, extract_field_data
 from desktop.lib.django_util import render, PopupException, extract_field_data
 from desktop.lib.rest.http_client import RestException
 from desktop.lib.rest.http_client import RestException
-from desktop.log.access import access_warn
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
-from jobsub.models import OozieDesign, OozieMapreduceAction, OozieStreamingAction,\
-  OozieJavaAction
+from jobsub.models import OozieDesign
 from liboozie.submittion import Submission
 from liboozie.submittion import Submission
 
 
 from oozie.conf import SHARE_JOBS
 from oozie.conf import SHARE_JOBS
 from oozie.import_jobsub import convert_jobsub_design
 from oozie.import_jobsub import convert_jobsub_design
 from oozie.management.commands import oozie_setup
 from oozie.management.commands import oozie_setup
-from oozie.models import Workflow, Node, Link, History, Coordinator,\
-  Mapreduce, Java, Streaming, Dataset, DataInput, DataOutput, Job,\
+from oozie.models import Job, Workflow, Node, Link, History, Coordinator,\
+  Mapreduce, Java, Streaming, Dataset, DataInput, DataOutput,\
   _STD_PROPERTIES_JSON
   _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,\
@@ -55,37 +52,6 @@ from oozie.forms import NodeForm, WorkflowForm, CoordinatorForm, DatasetForm,\
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
 
 
-"""
-Permissions:
-
-A Workflow/Coordinator can be accessed/submitted by its owner, a superuser or by anyone if its 'is_shared'
-property and SHARE_JOBS are set to True.
-
-A Workflow/Coordinator can be modified only by its owner or a superuser.
-
-Permissions checking happens by adding the decorators.
-"""
-def can_access_job(user, job):
-  return user.is_superuser or job.owner == user or (SHARE_JOBS.get() and job.is_shared)
-
-
-def can_access_job_or_exception(request, job_id):
-  if job_id is None:
-    return
-  try:
-    job = Job.objects.select_related().get(pk=job_id).get_full_node()
-    if can_access_job(request.user, job):
-      return job
-    else:
-      message = _("Permission denied. %(username)s don't have the permissions to access job %(id)s") % \
-          {'username': request.user.username, 'id': job.id}
-      access_warn(request, message)
-      request.error(message)
-      raise PopupException(message)
-
-  except Job.DoesNotExist:
-    raise PopupException(_('job %(id)s not exist') % {'id': job_id})
-
 
 
 def check_job_access_permission(view_func):
 def check_job_access_permission(view_func):
   """
   """
@@ -104,25 +70,13 @@ def check_job_access_permission(view_func):
 
 
     job = kwargs.get(job_type)
     job = kwargs.get(job_type)
     if job is not None:
     if job is not None:
-      job = can_access_job_or_exception(request, job)
+      job = Job.objects.is_accessible_or_exception(request, job)
     kwargs[job_type] = job
     kwargs[job_type] = job
 
 
     return view_func(request, *args, **kwargs)
     return view_func(request, *args, **kwargs)
   return wraps(view_func)(decorate)
   return wraps(view_func)(decorate)
 
 
 
 
-def can_edit_job(user, job):
-  """Only owners or admins can modify a job."""
-  return user.is_superuser or job.owner == user
-
-
-def can_edit_job_or_exception(request, job):
-  if can_edit_job(request.user, job):
-    return True
-  else:
-    raise PopupException('Not allowed to modified this job')
-
-
 def check_job_edition_permission(authorize_get=False):
 def check_job_edition_permission(authorize_get=False):
   """
   """
   Decorator ensuring that the user has the permissions to modify a workflow or coordinator.
   Decorator ensuring that the user has the permissions to modify a workflow or coordinator.
@@ -138,7 +92,7 @@ def check_job_edition_permission(authorize_get=False):
 
 
       job = kwargs.get(job_type)
       job = kwargs.get(job_type)
       if job is not None and not (authorize_get and request.method == 'GET'):
       if job is not None and not (authorize_get and request.method == 'GET'):
-        can_edit_job_or_exception(request, job)
+        Job.objects.can_edit_or_exception(request, job)
 
 
       return view_func(request, *args, **kwargs)
       return view_func(request, *args, **kwargs)
     return wraps(view_func)(decorate)
     return wraps(view_func)(decorate)
@@ -157,7 +111,7 @@ def check_action_access_permission(view_func):
   def decorate(request, *args, **kwargs):
   def decorate(request, *args, **kwargs):
     action_id = kwargs.get('action')
     action_id = kwargs.get('action')
     action = Node.objects.get(id=action_id).get_full_node()
     action = Node.objects.get(id=action_id).get_full_node()
-    can_access_job_or_exception(request, action.workflow.id)
+    Job.objects.is_accessible_or_exception(request, action.workflow.id)
     kwargs['action'] = action
     kwargs['action'] = action
 
 
     return view_func(request, *args, **kwargs)
     return view_func(request, *args, **kwargs)
@@ -172,28 +126,29 @@ def check_action_edition_permission(view_func):
   """
   """
   def decorate(request, *args, **kwargs):
   def decorate(request, *args, **kwargs):
     action = kwargs.get('action')
     action = kwargs.get('action')
-    can_edit_job_or_exception(request, action.workflow)
+    Job.objects.can_edit_or_exception(request, action.workflow)
 
 
     return view_func(request, *args, **kwargs)
     return view_func(request, *args, **kwargs)
   return wraps(view_func)(decorate)
   return wraps(view_func)(decorate)
 
 
 
 
-def can_access_dataset_or_exception(request, dataset_id):
-  if dataset_id is None:
-    return
-  try:
-    dataset = Dataset.objects.get(pk=dataset_id)
-    if can_access_job(request.user, dataset.coordinator):
-      return dataset
-    else:
-      message = _("Permission denied. %(username)s don't have the permissions to access dataset %(id)s") % \
-          {'username': request.user.username, 'id': dataset.id}
-      access_warn(request, message)
-      request.error(message)
-      raise PopupException(message)
+def check_dataset_access_permission(view_func):
+  """
+  Decorator ensuring that the user has access to dataset.
 
 
-  except Dataset.DoesNotExist:
-    raise PopupException(_('dataset %(id)s not exist') % {'id': dataset_id})
+  Arg: 'dataset'.
+  Return: the dataset or raise an exception
+
+  Notice: its gets an id in input and returns the full object in output (not an id).
+  """
+  def decorate(request, *args, **kwargs):
+    dataset = kwargs.get('dataset')
+    if dataset is not None:
+      dataset = Dataset.objects.is_accessible_or_exception(request, dataset)
+    kwargs['dataset'] = dataset
+
+    return view_func(request, *args, **kwargs)
+  return wraps(view_func)(decorate)
 
 
 
 
 def check_dataset_edition_permission(authorize_get=False):
 def check_dataset_edition_permission(authorize_get=False):
@@ -207,41 +162,37 @@ def check_dataset_edition_permission(authorize_get=False):
     def decorate(request, *args, **kwargs):
     def decorate(request, *args, **kwargs):
       dataset = kwargs.get('dataset')
       dataset = kwargs.get('dataset')
       if dataset is not None and not (authorize_get and request.method == 'GET'):
       if dataset is not None and not (authorize_get and request.method == 'GET'):
-        can_edit_job_or_exception(request, dataset.coordinator)
+        Job.objects.can_edit_or_exception(request, dataset.coordinator)
 
 
       return view_func(request, *args, **kwargs)
       return view_func(request, *args, **kwargs)
     return wraps(view_func)(decorate)
     return wraps(view_func)(decorate)
   return inner
   return inner
 
 
 
 
-def check_dataset_access_permission(view_func):
-  """
-  Decorator ensuring that the user has access to dataset.
+def list_workflows(request):
+  show_setup_app = True
+  data = Workflow.objects
 
 
-  Arg: 'dataset'.
-  Return: the dataset or raise an exception
+  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))
 
 
-  Notice: its gets an id in input and returns the full object in output (not an id).
-  """
-  def decorate(request, *args, **kwargs):
-    dataset = kwargs.get('dataset')
-    if dataset is not None:
-      dataset = can_access_dataset_or_exception(request, dataset)
-    kwargs['dataset'] = dataset
+  data = data.order_by('-last_modified')
 
 
-    return view_func(request, *args, **kwargs)
-  return wraps(view_func)(decorate)
+  return render('editor/list_workflows.mako', request, {
+    'jobs': list(data),
+    'currentuser': request.user,
+    'show_setup_app': show_setup_app,
+  })
 
 
 
 
-def list_workflows(request, job_type='workflow'):
+def list_coordinators(request, workflow_id=None):
   show_setup_app = True
   show_setup_app = True
 
 
-  if job_type == 'coordinators':
-    data = Coordinator.objects
-    template = "editor/list_coordinators.mako"
-  else:
-    data = Workflow.objects
-    template = "editor/list_workflows.mako"
+  data = Coordinator.objects
+  if workflow_id is not None:
+    data = data.filter(workflow__id=workflow_id)
 
 
   if not SHARE_JOBS.get() and not request.user.is_superuser:
   if not SHARE_JOBS.get() and not request.user.is_superuser:
     data = data.filter(owner=request.user)
     data = data.filter(owner=request.user)
@@ -250,7 +201,7 @@ def list_workflows(request, job_type='workflow'):
 
 
   data = data.order_by('-last_modified')
   data = data.order_by('-last_modified')
 
 
-  return render(template, request, {
+  return render('editor/list_coordinators.mako', request, {
     'jobs': list(data),
     'jobs': list(data),
     'currentuser': request.user,
     'currentuser': request.user,
     'show_setup_app': show_setup_app,
     'show_setup_app': show_setup_app,
@@ -279,9 +230,9 @@ def create_workflow(request):
 @check_job_access_permission
 @check_job_access_permission
 def edit_workflow(request, workflow):
 def edit_workflow(request, workflow):
   WorkflowFormSet = inlineformset_factory(Workflow, Node, form=NodeForm, max_num=0, can_order=False, can_delete=False)
   WorkflowFormSet = inlineformset_factory(Workflow, Node, form=NodeForm, max_num=0, can_order=False, can_delete=False)
-  history = History.objects.filter(submitter=request.user, job=workflow)
+  history = History.objects.filter(submitter=request.user, job=workflow).order_by('-submission_date')
 
 
-  if request.method == 'POST' and can_edit_job_or_exception(request, workflow):
+  if request.method == 'POST' and Job.objects.can_edit_or_exception(request, workflow):
     try:
     try:
       workflow_form = WorkflowForm(request.POST, instance=workflow)
       workflow_form = WorkflowForm(request.POST, instance=workflow)
       actions_formset = WorkflowFormSet(request.POST, request.FILES, instance=workflow)
       actions_formset = WorkflowFormSet(request.POST, request.FILES, instance=workflow)
@@ -306,7 +257,7 @@ def edit_workflow(request, workflow):
   actions_formset = WorkflowFormSet(instance=workflow)
   actions_formset = WorkflowFormSet(instance=workflow)
 
 
   graph_options = {}
   graph_options = {}
-  user_can_edit_job = can_edit_job(request.user, workflow)
+  user_can_edit_job = workflow.is_editable(request.user)
   if not user_can_edit_job:
   if not user_can_edit_job:
     graph_options = {'template': 'editor/gen/workflow-graph-readonly.xml.mako'}
     graph_options = {'template': 'editor/gen/workflow-graph-readonly.xml.mako'}
 
 
@@ -397,7 +348,7 @@ def resubmit_workflow(request, oozie_wf_id):
     raise PopupException(_('A POST request is required.'))
     raise PopupException(_('A POST request is required.'))
 
 
   history = History.objects.get(oozie_job_id=oozie_wf_id)
   history = History.objects.get(oozie_job_id=oozie_wf_id)
-  can_access_job_or_exception(request, history.job.id)
+  Job.objects.is_accessible_or_exception(request, history.job.id)
 
 
   workflow = history.get_workflow().get_full_node()
   workflow = history.get_workflow().get_full_node()
   properties = history.properties_dict
   properties = history.properties_dict
@@ -407,6 +358,15 @@ def resubmit_workflow(request, oozie_wf_id):
   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 schedule_workflow(request, workflow):
+  if Coordinator.objects.filter(workflow=workflow).exists():
+    request.info(_('You already have some coordinators for this workflow. Please submit one or create a new one.'))
+    return list_coordinators(request, workflow_id=workflow.id)
+  else:
+    return create_coordinator(request, workflow=workflow.id)
+
+
 @check_job_access_permission
 @check_job_access_permission
 def new_action(request, workflow, node_type, parent_action_id):
 def new_action(request, workflow, node_type, parent_action_id):
   ActionForm = design_form_by_type(node_type)
   ActionForm = design_form_by_type(node_type)
@@ -447,7 +407,7 @@ def new_action(request, workflow, node_type, parent_action_id):
 def edit_action(request, action):
 def edit_action(request, action):
   ActionForm = design_form_by_type(action.node_type)
   ActionForm = design_form_by_type(action.node_type)
 
 
-  if request.method == 'POST' and can_edit_job_or_exception(request, action.workflow):
+  if request.method == 'POST' and Job.objects.can_edit_or_exception(request, action.workflow):
     action_form = ActionForm(request.POST, instance=action)
     action_form = ActionForm(request.POST, instance=action)
     if action_form.is_valid():
     if action_form.is_valid():
       action = action_form.save()
       action = action_form.save()
@@ -466,7 +426,7 @@ def edit_action(request, action):
     'node_type': action.node_type,
     'node_type': action.node_type,
     'properties_hint': _STD_PROPERTIES_JSON,
     'properties_hint': _STD_PROPERTIES_JSON,
     'form_url': reverse('oozie:edit_action', kwargs={'action': action.id}),
     'form_url': reverse('oozie:edit_action', kwargs={'action': action.id}),
-    'can_edit_action': can_edit_job(request.user, action.workflow)
+    'can_edit_action': action.workflow.is_editable(request.user)
   })
   })
 
 
 
 
@@ -616,13 +576,13 @@ def delete_coordinator(request, coordinator):
   Submission(request.user, coordinator, request.fs, {}).remove_deployment_dir()
   Submission(request.user, coordinator, request.fs, {}).remove_deployment_dir()
   request.info(_('Coordinator deleted!'))
   request.info(_('Coordinator deleted!'))
 
 
-  return redirect(reverse('oozie:list_coordinator'))
+  return redirect(reverse('oozie:list_coordinators'))
 
 
 
 
 @check_job_access_permission
 @check_job_access_permission
 @check_job_edition_permission(True)
 @check_job_edition_permission(True)
 def edit_coordinator(request, coordinator):
 def edit_coordinator(request, coordinator):
-  history = History.objects.filter(submitter=request.user, job=coordinator)
+  history = History.objects.filter(submitter=request.user, job=coordinator).order_by('-submission_date')
 
 
   DatasetFormSet = inlineformset_factory(Coordinator, Dataset, form=DatasetForm, max_num=0, can_order=False, can_delete=True)
   DatasetFormSet = inlineformset_factory(Coordinator, Dataset, form=DatasetForm, max_num=0, can_order=False, can_delete=True)
   DataInputFormSet = inlineformset_factory(Coordinator, DataInput, form=DataInputSetForm, max_num=0, can_order=False, can_delete=True)
   DataInputFormSet = inlineformset_factory(Coordinator, DataInput, form=DataInputSetForm, max_num=0, can_order=False, can_delete=True)
@@ -672,7 +632,7 @@ def edit_coordinator(request, coordinator):
     'new_data_input_formset': new_data_input_formset,
     'new_data_input_formset': new_data_input_formset,
     'new_data_output_formset': new_data_output_formset,
     'new_data_output_formset': new_data_output_formset,
     'history': history,
     'history': history,
-    'can_edit_coordinator': can_edit_job(request.user, coordinator.workflow),
+    'can_edit_coordinator': coordinator.workflow.is_editable(request.user),
     'parameters': extract_field_data(coordinator_form['parameters'])
     'parameters': extract_field_data(coordinator_form['parameters'])
   })
   })
 
 
@@ -829,7 +789,7 @@ def resubmit_coordinator(request, oozie_coord_id):
     raise PopupException(_('A POST request is required.'))
     raise PopupException(_('A POST request is required.'))
 
 
   history = History.objects.get(oozie_job_id=oozie_coord_id)
   history = History.objects.get(oozie_job_id=oozie_coord_id)
-  can_access_job_or_exception(request, history.job.id)
+  Job.objects.is_accessible_or_exception(request, history.job.id)
 
 
   coordinator = history.get_coordinator().get_full_node()
   coordinator = history.get_coordinator().get_full_node()
   properties = history.properties_dict
   properties = history.properties_dict