text:
import json
import logging
import re
import time
import uuid

from datetime import datetime, timedelta
from dateutil.parser import parse
from string import Template

from django.core.urlresolvers import reverse
from django.utils.encoding import force_unicode
from django.utils.translation import ugettext as _

from desktop.lib import django_mako
from desktop.lib.exceptions_renderable import PopupException
from desktop.lib.i18n import smart_str
from desktop.lib.json_utils import JSONEncoderForHTML
from desktop.models import Document2

from hadoop.fs.hadoopfs import Hdfs
from liboozie.submission2 import Submission
from liboozie.submission2 import create_directories

from oozie.conf import REMOTE_SAMPLE_DIR
from oozie.utils import utc_datetime_format, UTC_TIME_FORMAT, convert_to_server_timezone
from hadoop.fs.exceptions import WebHdfsException

LOG = logging.getLogger(__name__)

class Job(object):

  def find_all_parameters(self, with_lib_path=True):
    params = self.find_parameters()

    for param in self.parameters:
      params[param['name'].strip()] = param['value']

    if params.get('nominal_time') == '':
      params['nominal_time'] = datetime.today().strftime(UTC_TIME_FORMAT)

    return  [{'name': name, 'value': value} for name, value in params.iteritems() if with_lib_path or name != 'oozie.use.system.libpath']

  @classmethod
  def get_workspace(cls, user):
    return (REMOTE_SAMPLE_DIR.get() + '/hue-oozie-$TIME').replace('$USER', user.username).replace('$TIME', str(time.time()))

  @property
  def validated_name(self):
    good_name = []

    for c in self.name[:40]:
      if not good_name:
        if not re.match('[a-zA-Z_\{\$\}]', c):
          c = '_'
      else:
        if not re.match('[\-_a-zA-Z0-9\{\$\}]', c):
          c = '_'
      good_name.append(c)

    return ''.join(good_name)

  def __str__(self):
    return '%s' % force_unicode(self.name)

class Workflow(Job):
  XML_FILE_NAME = 'workflow.xml'
  PROPERTY_APP_PATH = 'oozie.wf.application.path'
  SLA_DEFAULT = [
      {'key': 'enabled', 'value': False}, # Always first element
      {'key': 'nominal-time', 'value': '${nominal_time}'},
      {'key': 'should-start', 'value': ''},
      {'key': 'should-end', 'value': '${30 * MINUTES}'},
      {'key': 'max-duration', 'value': ''},
      {'key': 'alert-events', 'value': ''},
      {'key': 'alert-contact', 'value': ''},
      {'key': 'notification-msg', 'value': ''},
      {'key': 'upstream-apps', 'value': ''},
  ]
  HUE_ID = 'hue-id-w'

  def __init__(self, data=None, document=None, workflow=None):
    self.document = document

    if document is not None:
      self.data = document.data
    elif data is not None:
      self.data = data
    else:
      self.data = json.dumps({
          'layout': [{
              "size":12, "rows":[
                  {"widgets":[{"size":12, "name":"Start", "id":"3f107997-04cc-8733-60a9-a4bb62cebffc", "widgetType":"start-widget", "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span12"}]},
                  {"widgets":[{"size":12, "name":"End", "id":"33430f0f-ebfa-c3ec-f237-3e77efa03d0a", "widgetType":"end-widget", "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span12"}]},
                  {"widgets":[{"size":12, "name":"Kill", "id":"17c9c895-5a16-7443-bb81-f34b30b21548", "widgetType":"kill-widget", "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span12"}]}
              ],
              "drops":[ "temp"],
              "klass":"card card-home card-column span12"
          }],
          'workflow': workflow if workflow is not None else {
              "id": None,
              "uuid": None,
              "name": "My Workflow",
              "properties": {
                  "description": "",
                  "job_xml": "",
                  "sla_enabled": False,
                  "schema_version": "uri:oozie:workflow:0.5",
                  "properties": [],
                  "sla": Workflow.SLA_DEFAULT,
                  "show_arrows": True,
                  "wf1_id": None
              },
              "nodes":[
                  {"id":"3f107997-04cc-8733-60a9-a4bb62cebffc","name":"Start","type":"start-widget","properties":{},"children":[{'to': '33430f0f-ebfa-c3ec-f237-3e77efa03d0a'}]},
                  {"id":"33430f0f-ebfa-c3ec-f237-3e77efa03d0a","name":"End","type":"end-widget","properties":{},"children":[]},
                  {"id":"17c9c895-5a16-7443-bb81-f34b30b21548","name":"Kill","type":"kill-widget","properties":{'message': _('Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]')},"children":[]}
              ]
          }
      })

  @property
  def id(self):
    return self.document.id

  @property
  def uuid(self):
    return self.document.uuid

  def get_json(self):
    _data = self.get_data()

    return json.dumps(_data)

  def get_data(self):
    _data = json.loads(self.data)

    if self.document is not None:
      _data['workflow']['id'] = self.document.id
      _data['workflow']['dependencies'] = list(self.document.dependencies.values('uuid',))
    else:
      _data['workflow']['dependencies'] = []

    if 'parameters' not in _data['workflow']['properties']:
      _data['workflow']['properties']['parameters'] = [
          {'name': 'oozie.use.system.libpath', 'value': True},
      ]
    if 'show_arrows' not in _data['workflow']['properties']:
      _data['workflow']['properties']['show_arrows'] = True

    for node in _data['workflow']['nodes']:
      if 'credentials' in node['properties']: # If node is an Action
        if 'retry_max' not in node['properties']: # When displaying a workflow
          node['properties']['retry_max'] = []
        if 'retry_interval' not in node['properties']:
          node['properties']['retry_interval'] = []

      # Backward compatibility
      _upgrade_older_node(node)

    return _data

  def to_xml(self, mapping=None):
    if mapping is None:
      mapping = {}
    tmpl = 'editor2/gen/workflow.xml.mako'

    data = self.get_data()
    nodes = [node for node in self.nodes if node.name != 'End'] + [node for node in self.nodes if node.name == 'End'] # End at the end
    node_mapping = dict([(node.id, node) for node in nodes])

    sub_wfs_ids = [node.data['properties']['workflow'] for node in nodes if node.data['type'] == 'subworkflow']
    workflow_mapping = dict([(workflow.uuid, Workflow(document=workflow)) for workflow in Document2.objects.filter(uuid__in=sub_wfs_ids)])

    xml = re.sub(re.compile('\s*\n+', re.MULTILINE), '\n', django_mako.render_to_string(tmpl, {
              'wf': self,
              'workflow': data['workflow'],
              'nodes': nodes,
              'mapping': mapping,
              'node_mapping': node_mapping,
              'workflow_mapping': workflow_mapping
          }))
    return force_unicode(xml)

  @property
  def name(self):
    _data = self.get_data()
    return _data['workflow']['name']

  def update_name(self, name):
    _data = self.get_data()
    _data['workflow']['name'] = name
    self.data = json.dumps(_data)

  @property
  def deployment_dir(self):
    _data = self.get_data()
    return _data['workflow']['properties']['deployment_dir']

  @property
  def parameters(self):
    _data = self.get_data()
    return _data['workflow']['properties']['parameters']

  def override_subworkflow_id(self, sub_wf_action, workflow_id):
    _data = self.get_data()

    action = [_action for _action in _data['workflow']['nodes'] if _action['id'] == sub_wf_action.id]
    if action:
      action[0]['properties']['job_properties'].append({'name': Workflow.HUE_ID, 'value': workflow_id})

    self.data = json.dumps(_data)

  @property
  def sla_enabled(self):
    _data = self.get_data()
    return _data['workflow']['properties']['sla'][0].get('value')

  @property
  def has_some_slas(self):
    return self.sla_enabled or any([node.sla_enabled for node in self.nodes])

  @property
  def credentials(self):
    return list(set([cred for node in self.nodes for cred in node.data['properties']['credentials']]))

  @property
  def sla(self):
    _data = self.get_data()
    return _data['workflow']['properties']['sla']

  @property
  def nodes(self):
    _data = self.get_data()
    return [Node(node) for node in _data['workflow']['nodes']]

  def find_parameters(self):
    params = set()

    for param in find_dollar_braced_variables(self.name):
      params.add(param)

    if self.sla_enabled:
      for param in find_json_parameters(self.sla):
        params.add(param)

    for node in self.nodes:
      params.update(node.find_parameters())

    return dict([(param, '') for param in list(params)])

  def set_workspace(self, user):
    _data = json.loads(self.data)

    _data['workflow']['properties']['deployment_dir'] = Job.get_workspace(user)

    self.data = json.dumps(_data)

  def check_workspace(self, fs, user):
    # Create optional default root workspace for the first submission
    if REMOTE_SAMPLE_DIR.get() == REMOTE_SAMPLE_DIR.config.default_value:
      create_directories(fs, [REMOTE_SAMPLE_DIR.get()])

    Submission(user, self, fs, None, {})._create_dir(self.deployment_dir)
    Submission(user, self, fs, None, {})._create_dir(Hdfs.join(self.deployment_dir, 'lib'))

  def import_workspace(self, fs, source_deployment_dir, owner):
    try:
      fs.copy_remote_dir(source_deployment_dir, self.deployment_dir, owner=owner)
    except WebHdfsException, e:
      msg = _('The copy of the deployment directory failed: %s.') % e
      LOG.error(msg)
      raise PopupException(msg)

  def create_single_action_workflow_data(self, node_id):
    _data = json.loads(self.data)

    start_node = [node for node in _data['workflow']['nodes'] if node['name'] == 'Start'][0]
    submit_node = [node for node in _data['workflow']['nodes'] if node['id'] == node_id][0]
    end_node = [node for node in _data['workflow']['nodes'] if node['name'] == 'End'][0]
    kill_node = [node for node in _data['workflow']['nodes'] if node['name'] == 'Kill'][0]

    # Modify children to point Start -> Submit_node -> End/Kill
    start_node['children'] = [{'to': submit_node['id']}]
    submit_node['children'] = [{'to': end_node['id']}, {'error': kill_node['id']}]
    _data['workflow']['properties']['deployment_dir'] = None

    # Recursively find the widget node
    def _get_node(rows, node_id):
      for row in rows:
        if not row['widgets']:
          for col in row['columns']:
            node = _get_node(col['rows'], node_id)
            if node:
              return node
        elif row['widgets'][0]['id'] == node_id:
          return row

    # Create wf data with above nodes
    return json.dumps({
      'layout': [{
          "size": 12,
          "rows": [
              [row for row in _data['layout'][0]['rows'] if row['widgets'] and row['widgets'][0]['name'] == 'Start'][0],
              _get_node(_data['layout'][0]['rows'], node_id),
              [row for row in _data['layout'][0]['rows'] if row['widgets'] and row['widgets'][0]['name'] == 'End'][0],
              [row for row in _data['layout'][0]['rows'] if row['widgets'] and row['widgets'][0]['name'] == 'Kill'][0]
          ],
          "drops": ["temp"],
          "klass": "card card-home card-column span12"
      }],
      'workflow': {
          "id": None,
          "uuid": None,
          "name": _data['workflow']['name'],
          "properties": _data['workflow']['properties'],
          "nodes": [start_node, submit_node, end_node, kill_node]
      }
    })

  def get_absolute_url(self):
    return reverse('oozie:edit_workflow') + '?workflow=%s' % self.id

  @classmethod
  def get_application_path_key(cls):
    return 'oozie.wf.application.path'

class Node():
  def __init__(self, data):
    self.data = data

    self._augment_data()

  def to_xml(self, mapping=None, node_mapping=None, workflow_mapping=None):
    if mapping is None:
      mapping = {}
    if node_mapping is None:
      node_mapping = {}
    if workflow_mapping is None:
      workflow_mapping = {}

    if self.data['type'] == 'hive2' and not self.data['properties']['jdbc_url']:
      self.data['properties']['jdbc_url'] = _get_hiveserver2_url()

    if self.data['type'] == 'fork':
      links = [link for link in self.data['children'] if link['to'] in node_mapping]
      if len(links) != len(self.data['children']):
        LOG.warn('Fork has some children links that do not exist, ignoring them: links %s, existing links %s, links %s, existing links %s' \
                 % (len(links), len(self.data['children']), links, self.data['children']))
        self.data['children'] = links

    data = {
      'node': self.data,
      'mapping': mapping,
      'node_mapping': node_mapping,
      'workflow_mapping': workflow_mapping
    }

    return django_mako.render_to_string(self.get_template_name(), data)

  @property
  def id(self):
    return self.data['id']

  @property
  def name(self):
    return self.data['name']

  @property
  def sla_enabled(self):
    return 'sla' in self.data['properties'] and self.data['properties']['sla'] and self.data['properties']['sla'][0].get('value')

  def _augment_data(self):
    self.data['type'] = self.data['type'].replace('-widget', '')
    self.data['uuid'] = self.data['id']

    # Action Node
    if 'credentials' not in self.data['properties']:
      self.data['properties']['credentials'] = []
    if 'prepares' not in self.data['properties']:
      self.data['properties']['prepares'] = []
    if 'job_xml' not in self.data['properties']:
      self.data['properties']['job_xml'] = []
    if 'properties' not in self.data['properties']:
      self.data['properties']['properties'] = []
    if 'params' not in self.data['properties']:
      self.data['properties']['params'] = []
    if 'files' not in self.data['properties']:
      self.data['properties']['files'] = []
    if 'archives' not in self.data['properties']:
      self.data['properties']['archives'] = []
    if 'sla' not in self.data['properties']:
      self.data['properties']['sla'] = Workflow.SLA_DEFAULT
    if 'retry_max' not in self.data['properties']:
      self.data['properties']['retry_max'] = []
    if 'retry_interval' not in self.data['properties']:
      self.data['properties']['retry_interval'] = []

    # Backward compatibility
    _upgrade_older_node(self.data)

  def get_template_name(self):
    return 'editor2/gen/workflow-%s.xml.mako' % self.data['type']

  def find_parameters(self):
    return find_parameters(self) + (find_parameters(self, ['sla']) if self.sla_enabled else [])

def _upgrade_older_node(node):
  if node['type'] in ('sqoop', 'sqoop-widget') and 'arguments' not in node['properties']:
    node['properties']['arguments'] = node['properties']['parameters']

  if node['type'] in ('kill', 'kill-widget') and 'to' not in node['properties']:
    node['properties']['enableMail'] = False
    node['properties']['to'] = ''
    node['properties']['cc'] = ''
    node['properties']['subject'] = ''
    node['properties']['body'] = ''

class Action(object):

  @classmethod
  def get_fields(cls):
    credentials = [cls.DEFAULT_CREDENTIALS] if hasattr(cls, 'DEFAULT_CREDENTIALS') else []
    return [(f['name'], f['value']) for f in cls.FIELDS.itervalues()] + [('sla', Workflow.SLA_DEFAULT), ('credentials', credentials)]

class StartNode(Action):
  TYPE = 'start'
  FIELDS = {}

class EndNode(Action):
  TYPE = 'end'
  FIELDS = {}

class PigAction(Action):
  TYPE = 'pig'
  FIELDS = {
     'script_path': {
          'name': 'script_path',
          'label': _('Script'),
          'value': '',
          'help_text': _('Path to the script on HDFS.'),
          'type': ''
     },
     'parameters': {
          'name': 'parameters',
          'label': _('Parameters'),
          'value': [],
          'help_text': _('The Pig parameters of the script without -param. e.g. INPUT=${inputDir}'),
          'type': ''
     },
     'arguments': {
          'name': 'arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('The Pig parameters of the script as is. e.g. -param, INPUT=${inputDir}'),
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': [],
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['script_path']]

class JavaAction(Action):
  TYPE = 'java'
  FIELDS = {
     'jar_path': {
          'name': 'jar_path',
          'label': _('Jar name'),
          'value': '',
          'help_text': _('Path to the jar on HDFS.'),
          'type': ''
     },
     'main_class': {
          'name': 'main_class',
          'label': _('Main class'),
          'value': '',
          'help_text': _('Java class. e.g. org.apache.hadoop.examples.Grep'),
          'type': 'text'
     },
     'arguments': {
          'name': 'arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('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.'),
          'type': ''
     },
     'java_opts': {
          'name': 'java_opts',
          'label': _('Java options'),
          'value': [],
          'help_text': _('Parameters for the JVM, e.g. -Dprop1=a -Dprop2=b'),
          'type': ''
     },
     'capture_output': {
          'name': 'capture_output',
          'label': _('Capture output'),
          'value': False,
          'help_text': _('Capture output of the stdout of the %(program)s command execution. The %(program)s '
                         'command output must be in Java Properties file format and it must not exceed 2KB. '
                         'From within the workflow definition, the output of an %(program)s action node is accessible '
                         'via the String action:output(String node, String key) function') % {'program': TYPE.title()},
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': [],
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['jar_path'], cls.FIELDS['main_class']]

class HiveAction(Action):
  TYPE = 'hive'
  DEFAULT_CREDENTIALS = 'hcat'
  FIELDS = {
     'script_path': {
          'name': 'script_path',
          'label': _('Script'),
          'value': '',
          'help_text': _('Path to the script on HDFS.'),
          'type': ''
     },
     'parameters': {
          'name': 'parameters',
          'label': _('Parameters'),
          'value': [],
          'help_text': _('The %(type)s parameters of the script. E.g. N=5, INPUT=${inputDir}')  % {'type': TYPE.title()},
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'hive_xml': {
          'name': 'hive_xml',
          'label': _('Hive XML'),
          'value': '',
          'help_text': _('Refer to a hive-site.xml for connecting to Hive'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['script_path'], cls.FIELDS['hive_xml']]

def _get_hiveserver2_url():
  try:
    from beeswax.conf import HIVE_SERVER_HOST, HIVE_SERVER_PORT
    return 'jdbc:hive2://%s:%s/default' % (HIVE_SERVER_HOST.get(), HIVE_SERVER_PORT.get())
  except Exception, e:
    # Might fail is Hive is disabled
    LOG.warn('Could not guess HiveServer2 URL: %s' % smart_str(e))
    return 'jdbc:hive2://localhost:10000/default'

class HiveServer2Action(Action):
  TYPE = 'hive2'
  DEFAULT_CREDENTIALS = 'hive2'
  FIELDS = {
     'script_path': {
          'name': 'script_path',
          'label': _('Script'),
          'value': '',
          'help_text': _('Path to the script on HDFS.'),
          'type': ''
     },
     'parameters': {
          'name': 'parameters',
          'label': _('Parameters'),
          'value': [],
          'help_text': _('The %(type)s parameters of the script. E.g. N=5, INPUT=${inputDir}')  % {'type': TYPE.title()},
          'type': ''
     },
     # Common
     'jdbc_url': {
          'name': 'jdbc_url',
          'label': _('HiveServer2 URL'),
          'value': "",
          'help_text': _('e.g. jdbc:hive2://localhost:10000/default. JDBC URL for the Hive Server 2.'),
          'type': ''
     },
     'password': {
          'name': 'password',
          'label': _('Password'),
          'value': '',
          'help_text': _('The password element must contain the password of the current user. However, the password is only used if Hive Server 2 is backed by '
                         'something requiring a password (e.g. LDAP); non-secured Hive Server 2 or Kerberized Hive Server 2 don\'t require a password.'),
          'type': ''
     },
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['script_path']]

class SubWorkflowAction(Action):
  TYPE = 'subworkflow'
  FIELDS = {
     'workflow': {
          'name': 'workflow',
          'label': _('Sub-workflow'),
          'value': None,
          'help_text': _('The sub-workflow application to include. You must own all the sub-workflows'),
          'type': 'workflow'
     },
     'propagate_configuration': {
          'name': 'propagate_configuration',
          'label': _('Propagate configuration'),
          'value': True,
          'help_text': _('If the workflow job configuration should be propagated to the child workflow.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('Can be used to specify the job properties that are required to run the child workflow job.'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['workflow']]

class SqoopAction(Action):
  TYPE = 'sqoop'
  FIELDS = {
     'command': {
          'name': 'command',
          'label': _('Sqoop command'),
          'value': 'import  --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir hdfs://localhost:8020/user/foo -m 1',
          'help_text': _('The full %(type)s command. Either put it here or split it by spaces and insert the parts as multiple parameters below.') % {'type': TYPE},
          'type': 'textarea'
     },
     'arguments': {
          'name': 'arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('If no command is specified, split the command by spaces and insert the %(type)s parameters '
                         'here e.g. import, --connect, jdbc:hsqldb:file:db.hsqldb, ...') % {'type': TYPE},
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['command']]

class MapReduceAction(Action):
  TYPE = 'mapreduce'
  FIELDS = {
     'jar_path': {
          'name': 'jar_path',
          'label': _('Jar name'),
          'value': '',
          'help_text': _('Path to the jar on HDFS.'),
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['jar_path']]

class ShellAction(Action):
  TYPE = 'shell'
  FIELDS = {
     'shell_command': {
          'name': 'shell_command',
          'label': _('Shell command'),
          'value': '',
          'help_text': _('Shell command to execute, e.g script.sh'),
          'type': ''
     },
     'arguments': {
          'name': 'arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('One arg, e.g. -l, --help'),
          'type': ''
     },
     'env_var': {
          'name': 'env_var',
          'label': _('Environment variables'),
          'value': [],
          'help_text': _('e.g. MAX=10 or PATH=$PATH:mypath'),
          'type': ''
     },
     'capture_output': {
          'name': 'capture_output',
          'label': _('Capture output'),
          'value': True,
          'help_text': _('Capture output of the stdout of the %(program)s command execution. The %(program)s '
                         'command output must be in Java Properties file format and it must not exceed 2KB. '
                         'From within the workflow definition, the output of an %(program)s action node is accessible '
                         'via the String action:output(String node, String key) function') % {'program': TYPE},
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.'),
          'type': ''
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.'),
          'type': ''
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production'),
          'type': ''
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.'),
          'type': ''
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['shell_command']]

class SshAction(Action):
  TYPE = 'ssh'
  FIELDS = {
     'host': {
          'name': 'host',
          'label': _('User and Host'),
          'value': 'user@host.com',
          'help_text': _('Where the shell will be executed.'),
          'type': 'text'
     },
     'ssh_command': {
          'name': 'ssh_command',
          'label': _('Ssh command'),
          'value': 'ls',
          'help_text': _('The path of the Shell command to execute.'),
          'type': 'textarea'
     },
     'arguments': {
          'name': 'arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('One arg, e.g. -l, --help'),
          'type': ''
     },
     'capture_output': {
          'name': 'capture_output',
          'label': _('Capture output'),
          'value': True,
          'help_text': _('Capture output of the stdout of the %(program)s command execution. The %(program)s '
                         'command output must be in Java Properties file format and it must not exceed 2KB. '
                         'From within the workflow definition, the output of an %(program)s action node is accessible '
                         'via the String action:output(String node, String key) function') % {'program': TYPE},
          'type': ''
     },
     # Common
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['host'], cls.FIELDS['ssh_command']]

class FsAction(Action):
  TYPE = 'fs'
  FIELDS = {
     'deletes': {
          'name': 'deletes',
          'label': _('Delete path'),
          'value': [],
          'help_text': _('Deletes recursively all content.'),
          'type': ''
     },
     'mkdirs': {
          'name': 'mkdirs',
          'label': _('Create directory'),
          'value': [],
          'help_text': _('Sub directories are created if needed.'),
          'type': ''
     },
     'moves': {
          'name': 'moves',
          'label': _('Move file or directory'),
          'value': [],
          'help_text': _('Destination.'),
          'type': ''
     },
     'chmods': {
          'name': 'chmods',
          'label': _('Change permissions'),
          'value': [],
          'help_text': _('File or directory.'),
          'type': ''
     },
     'touchzs': {
          'name': 'touchzs',
          'label': _('Create or touch a file'),
          'value': [],
          'help_text': _('Or update its modification date.'),
          'type': ''
     },
     'chgrps': {
          'name': 'chgrps',
          'label': _('Change the group'),
          'value': [],
          'help_text': _('File or directory.'),
          'type': ''
     },
     # Common
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return []

class EmailAction(Action):
  TYPE = 'email'
  FIELDS = {
     'to': {
          'name': 'to',
          'label': _('To addresses'),
          'value': '',
          'help_text': _('Comma-separated values'),
          'type': 'text'
     },
     'cc': {
          'name': 'cc',
          'label': _('Cc addresses (optional)'),
          'value': '',
          'help_text': _('Comma-separated values'),
          'type': 'text'
     },
     'subject': {
          'name': 'subject',
          'label': _('Subject'),
          'value': '',
          'help_text': _('Plain-text'),
          'type': 'text'
     },
     'body': {
          'name': 'body',
          'label': _('Body'),
          'value': '',
          'help_text': _('Plain-text'),
          'type': 'textarea'
     },
     # Common
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['to'], cls.FIELDS['subject'], cls.FIELDS['body']]

class StreamingAction(Action):
  TYPE = 'streaming'
  FIELDS = {
     'mapper': {
          'name': 'mapper',
          'label': _('Mapper'),
          'value': '',
          'help_text': _('The executable/script to be used as mapper.'),
          'type': ''
     },
     'reducer': {
          'name': 'reducer',
          'label': _('Reducer'),
          'value': '',
          'help_text': _('The executable/script to be used as reducer.'),
          'type': ''
     },
     # Common
     'files': {
          'name': 'files',
          'label': _('Files'),
          'value': [],
          'help_text': _('Files put in the running directory.')
     },
     'archives': {
          'name': 'archives',
          'label': _('Archives'),
          'value': [],
          'help_text': _('zip, tar and tgz/tar.gz uncompressed into the running directory.')
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production')
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.')
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml')
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['mapper'], cls.FIELDS['reducer']]

class DistCpAction(Action):
  TYPE = 'distcp'
  FIELDS = {
     'distcp_parameters': {
          'name': 'distcp_parameters',
          'label': _('Arguments'),
          'value': [{'value': ''}, {'value': ''}],
          'help_text': _('Options first, then source / destination paths'),
          'type': 'distcp'
     },
      # Common
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.')
     },
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production')
     },
     'java_opts': {
          'name': 'java_opts',
          'label': _('Java options'),
          'value': '',
          'help_text': _('Parameters for the JVM, e.g. -Dprop1=a -Dprop2=b')
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['distcp_parameters']]

class SparkAction(Action):
  TYPE = 'spark'
  FIELDS = {
     'spark_master': {
          'name': 'spark_master',
          'label': _('Spark Master'),
          'value': 'local[*]',
          'help_text': _('Ex: spark://host:port, mesos://host:port, yarn, or local.'),
          'type': ''
     },
     'mode': {
          'name': 'mode',
          'label': _('Mode'),
          'value': 'client',
          'help_text': _('e.g. client,cluster'),
          'type': ''
     },
     'app_name': {
          'name': 'app_name',
          'label': _('App name'),
          'value': 'MySpark',
          'help_text': _('The name of the spark application'),
          'type': ''
     },
    'class': {
          'name': 'class',
          'label': _('Main class'),
          'value': '',
          'help_text': _("e.g. org.apache.spark.examples.mllib.JavaALS."),
          'type': 'text'
     },
     'jars': {
          'name': 'jars',
          'label': _('Jars/py files'),
          'value': '',
          'help_text': _('Comma separated list of jars or python HDFS files.'),
          'type': ''
     },
     'spark_opts': {
          'name': 'spark_opts',
          'label': _('Options list'),
          'value': '',
          'help_text': _('Ex: --executor-memory 20G --num-executors 50'),
          'type': ''
     },
     'spark_arguments': {
          'name': 'spark_arguments',
          'label': _('Arguments'),
          'value': [],
          'help_text': _('Arguments, one by one, e.g. 1000, /path/a.')
     },
     # Common
     'job_properties': {
          'name': 'job_properties',
          'label': _('Hadoop job properties'),
          'value': [],
          'help_text': _('value, e.g. production')
     },
     'prepares': {
          'name': 'prepares',
          'label': _('Prepares'),
          'value': [],
          'help_text': _('Path to manipulate before starting the application.')
     },
     'job_xml': {
          'name': 'job_xml',
          'label': _('Job XML'),
          'value': '',
          'help_text': _('Refer to a Hadoop JobConf job.xml'),
          'type': ''
     },
     'retry_max': {
          'name': 'retry_max',
          'label': _('Max retry'),
          'value': [],
          'help_text': _('Number of times, default is 3'),
          'type': ''
     },
     'retry_interval': {
          'name': 'retry_interval',
          'label': _('Retry interval'),
          'value': [],
          'help_text': _('Wait time in minutes, default is 10'),
          'type': ''
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['spark_master'], cls.FIELDS['mode'], cls.FIELDS['jars'], cls.FIELDS['class']]

class KillAction(Action):
  TYPE = 'kill'
  FIELDS = {
     'message': {
          'name': 'message',
          'label': _('Message'),
          'value': _('Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]'),
          'help_text': _('Message to display when the workflow fails. Can contain some EL functions.'),
          'type': 'textarea'
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['message']]

class JoinAction(Action):
  TYPE = 'join'
  FIELDS = {}

  @classmethod
  def get_mandatory_fields(cls):
    return []

class GenericAction(Action):
  TYPE = 'generic'
  FIELDS = {
     'xml': {
          'name': 'xml',
          'label': _('XML of the action'),
          'value': '<my_action>\n</my_action>',
          'help_text': _('Insert verbatim the XML of the action to insert into the workflow.'),
          'type': 'textarea'
     }
  }

  @classmethod
  def get_mandatory_fields(cls):
    return [cls.FIELDS['xml']]

class ForkNode(Action):
  TYPE = 'fork'
  FIELDS = {}

  @classmethod
  def get_mandatory_fields(cls):
    return []

class DecisionNode(Action):
  TYPE = 'decision'
  FIELDS = {}

  @classmethod
  def get_mandatory_fields(cls):
    return []

NODES = {
  'start-widget': StartNode,
  'end-widget': EndNode,
  'pig-widget': PigAction,
  'java-widget': JavaAction,
  'hive-widget': HiveAction,
  'hive2-widget': HiveServer2Action,
  'sqoop-widget': SqoopAction,
  'mapreduce-widget': MapReduceAction,
  'subworkflow-widget': SubWorkflowAction,
  'shell-widget': ShellAction,
  'ssh-widget': SshAction,
  'fs-widget': FsAction,
  'email-widget': EmailAction,
  'streaming-widget': StreamingAction,
  'distcp-widget': DistCpAction,
  'kill-widget': KillAction,
  'join-widget': JoinAction,
  'fork-widget': ForkNode,
  'decision-widget': DecisionNode,
  'spark-widget': SparkAction,
  'generic-widget': GenericAction
}

WORKFLOW_NODE_PROPERTIES = {}
for node in NODES.itervalues():
  WORKFLOW_NODE_PROPERTIES.update(node.FIELDS)

def find_parameters(instance, fields=None):
  """Find parameters in the given fields"""
  if fields is None:
    fields = NODES['%s-widget' % instance.data['type']].FIELDS.keys()

  params = []
  for field in fields:
    data = instance.data['properties'][field]
    if field == 'sla' and not instance.sla_enabled:
      continue
    if isinstance(data, list):
      params.extend(find_json_parameters(data))
    elif isinstance(data, basestring):
      for match in Template.pattern.finditer(data):
        name = match.group('braced')
        if name is not None:
          params.append(name)

  return params

def find_json_parameters(fields):
  # Input is list of json dict
  params = []

  for field in fields:
    for data in field.values():
      if isinstance(data, basestring):
        for match in Template.pattern.finditer(data):
          name = match.group('braced')
          if name is not None:
            params.append(name)

  return params

def find_dollar_variables(text):
  return re.findall('[^\n\\\\]\$([^\{ \'\"\-;\(\)]+)', text, re.MULTILINE)

def find_dollar_braced_variables(text):
  vars = set()

  for var in re.findall('\$\{(.+)\}', text, re.MULTILINE):
    if ':' in var:
      var = var.split(':', 1)[1]
    vars.add(var)

  return list(vars)

def import_workflow_from_hue_3_7(old_wf):
  """
  Example of data to transform

  [<Start: start>, <Pig: Pig>, [<Kill: kill>], [<End: end>]]
  [<Start: start>, <Java: TeraGenWorkflow>, <Java: TeraSort>, [<Kill: kill>], [<End: end>]]
  [<Start: start>, [<Fork: fork-34>, [[<Mapreduce: Sleep-1>, <Mapreduce: Sleep-10>], [<Mapreduce: Sleep-5>, [<Fork: fork-38>, [[<Mapreduce: Sleep-3>], [<Mapreduce: Sleep-4>]], <Join: join-39>]]], <Join: join-35>], [<Kill: kill>], [<End: end>]]
  """

  uuids = {}

  old_nodes = old_wf.get_hierarchy()

  wf = Workflow()
  wf_rows = []
  wf_nodes = []

  data = wf.get_data()

  # UUIDs node mapping
  for node in old_wf.node_list:
    if node.name == 'kill':
      node_uuid = '17c9c895-5a16-7443-bb81-f34b30b21548'
    elif node.name == 'start':
      node_uuid = '3f107997-04cc-8733-60a9-a4bb62cebffc'
    elif node.name == 'end':
      node_uuid = '33430f0f-ebfa-c3ec-f237-3e77efa03d0a'
    else:
      node_uuid = str(uuid.uuid4())

    uuids[node.id] = node_uuid

  # Workflow
  data['workflow']['uuid'] = str(uuid.uuid4())
  data['workflow']['name'] = old_wf.name
  data['workflow']['properties']['properties'] = json.loads(old_wf.job_properties)
  data['workflow']['properties']['job_xml'] = old_wf.job_xml
  data['workflow']['properties']['description'] = old_wf.description
  data['workflow']['properties']['schema_version'] = old_wf.schema_version
  data['workflow']['properties']['deployment_dir'] = old_wf.deployment_dir
  data['workflow']['properties']['parameters'] = json.loads(old_wf.parameters)
  data['workflow']['properties']['description'] = old_wf.description
  data['workflow']['properties']['sla'] = old_wf.sla
  data['workflow']['properties']['sla_enabled'] = old_wf.sla_enabled
  data['workflow']['properties']['imported'] = True
  data['workflow']['properties']['wf1_id'] = old_wf.id

  # Layout
  rows = data['layout'][0]['rows']

  def _create_layout(nodes, size=12):
    wf_rows = []

    for node in nodes:
      if type(node) == list and len(node) == 1:
        node = node[0]
      if type(node) != list:
        wf_rows.append({"widgets":[{"size":size, "name": node.name.title(), "id":  uuids[node.id], "widgetType": "%s-widget" % node.node_type, "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span%s" % size, "columns":[]}]})
      else:
        if node[0].node_type == 'fork':
          wf_rows.append({"widgets":[{"size":size, "name": 'Fork', "id":  uuids[node[0].id], "widgetType": "%s-widget" % node[0].node_type, "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span%s" % size, "columns":[]}]})

          wf_rows.append({
            "id": str(uuid.uuid4()),
            "widgets":[

            ],
            "columns":[
               {
                  "id": str(uuid.uuid4()),
                  "size": (size / len(node[1])),
                  "rows":
                     [{
                        "id": str(uuid.uuid4()),
                        "widgets": c['widgets'],
                        "columns":[]
                      }
                    for c in col] if type(col) == list else [{
                        "id": str(uuid.uuid4()),
                        "widgets": col['widgets'],
                        "columns":[]
                      }
                   ]
                  ,
                  "klass":"card card-home card-column span%s" % (size / len(node[1]))
               }
               for col in _create_layout(node[1], size)
            ]
          })

          wf_rows.append({"widgets":[{"size":size, "name": 'Join', "id":  uuids[node[2].id], "widgetType": "%s-widget" % node[2].node_type, "properties":{}, "offset":0, "isLoading":False, "klass":"card card-widget span%s" % size, "columns":[]}]})
        else:
          wf_rows.append(_create_layout(node, size))

    return wf_rows

  wf_rows = _create_layout(old_nodes)

  if wf_rows:
    data['layout'][0]['rows'] = [data['layout'][0]['rows'][0]] + wf_rows + [data['layout'][0]['rows'][-1]]

  # Content
  def _dig_nodes(nodes):
    for node in nodes:
      if type(node) != list:
        properties = {}
        if '%s-widget' % node.node_type in NODES:
          properties = dict(NODES['%s-widget' % node.node_type].get_fields())

        if node.node_type == 'pig':
          properties['script_path'] = node.script_path
          properties['parameters'] = [param for param in json.loads(node.params) if param['value'] != '-param']
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'hive':
          properties['script_path'] = node.script_path
          properties['parameters'] = [param for param in json.loads(node.params) if param['value'] != '-param']
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['hive_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'java':
          properties['jar_path'] = node.jar_path
          properties['main_class'] = node.main_class
          properties['arguments'] = [{'value': arg} for arg in node.args.split(' ')]
          properties['java_opts'] = node.java_opts
          properties['capture_output'] = node.capture_output
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'sqoop':
          properties['command'] = node.script_path
          properties['parameters'] = json.loads(node.params)
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'mapreduce':
          properties['jar_path'] = node.jar_path
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'shell':
          properties['shell_command'] = node.command
          properties['arguments'] = json.loads(node.params)
          properties['capture_output'] = node.capture_output
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'ssh':
          properties['user'] = '%s@%s' % (node.user, node.host)
          properties['ssh_command'] = node.command
          properties['params'] = json.loads(node.params)
          properties['capture_output'] = node.capture_output
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'fs':
          properties['deletes'] = [{'value': f['name']} for f in json.loads(node.deletes)]
          properties['mkdirs'] = [{'value': f['name']} for f in json.loads(node.mkdirs)]
          properties['moves'] = json.loads(node.moves)
          chmods = json.loads(node.chmods)
          for c in chmods:
            c['value'] = c['path']
            c['dir_files'] = False
          properties['chmods'] = chmods
          properties['touchzs'] = [{'value': f['name']} for f in json.loads(node.touchzs)]
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'email':
          properties['to'] = node.to
          properties['cc'] = node.cc
          properties['subject'] = node.subject
          properties['body'] = node.body
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'streaming':
          properties['mapper'] = node.mapper
          properties['reducer'] = node.reducer
          properties['files'] = [{'value': f} for f in json.loads(node.files)]
          properties['archives'] = json.loads(node.archives)
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['job_xml'] = node.job_xml
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled
        elif node.node_type == 'distcp':
          properties['distcp_parameters'] = json.loads(node.params)
          properties['java_opts'] = node.job_xml
          properties['job_properties'] = json.loads(node.job_properties)
          properties['prepares'] = json.loads(node.prepares)
          properties['description'] = node.description
          properties['sla'] = node.sla
          properties['sla_enabled'] = node.sla_enabled

        wf_nodes.append({
            "id": uuids[node.id],
            "name": '%s-%s' % (node.node_type.split('-')[0], uuids[node.id][:4]),
            "type": "%s-widget" % node.node_type,
            "properties": properties,
            "children":[{('to' if link.name in ('ok', 'start') else link.name): uuids[link.child.get_full_node().id]} for link in node.get_children_links()]
        })
      else:
        _dig_nodes(node)

  _dig_nodes(old_nodes)

  data['workflow']['nodes'] = wf_nodes

  return Workflow(data=json.dumps(data))

class Coordinator(Job):
  XML_FILE_NAME = 'coordinator.xml'
  PROPERTY_APP_PATH = 'oozie.coord.application.path'
  HUE_ID = 'hue-id-c'

  def __init__(self, data=None, json_data=None, document=None):
    self.document = document

    if document is not None:
      self._data = json.loads(document.data)
    elif json_data is not None:
      self._data = json.loads(json_data)
    elif data is not None:
      self._data = data
    else:
      self._data = {
          'id': None,
          'uuid': None,
          'name': 'My Coordinator',
          'variables': [], # Aka workflow parameters
          'properties': {
              'description': '',
              'deployment_dir': '',
              'schema_version': 'uri:oozie:coordinator:0.2',
              'frequency_number': 1,
              'frequency_unit': 'days',
              'cron_frequency': '0 0 * * *',
              'cron_advanced': False,
              'timezone': '',
              'start': '${start_date}',
              'end': '${end_date}',
              'workflow': None,
              'timeout': None,
              'concurrency': None,
              'execution': None,
              'throttle': None,
              'job_xml': '',
              'credentials': [],
              'parameters': [
                  {'name': 'oozie.use.system.libpath', 'value': True},
                  {'name': 'start_date', 'value':  datetime.today().strftime('%Y-%m-%dT%H:%M')},
                  {'name': 'end_date', 'value': (datetime.today() + timedelta(days=7)).strftime('%Y-%m-%dT%H:%M')}
              ],
              'sla': Workflow.SLA_DEFAULT
          }
      }

  @property
  def id(self):
    return self.document.id

  @property
  def uuid(self):
    return self.document.uuid

  def get_data_for_json(self):
    _data = self.data.copy()

    start_date = filter(lambda a: a['name'] == 'start_date', self._data['properties']['parameters'])
    if start_date and type(start_date[0]['value']) == datetime:
      start_date[0]['value'] = start_date[0]['value'].strftime('%Y-%m-%dT%H:%M:%S')

    end_date = filter(lambda a: a['name'] == 'end_date', self._data['properties']['parameters'])
    if end_date and type(end_date[0]['value']) == datetime:
      end_date[0]['value'] = end_date[0]['value'].strftime('%Y-%m-%dT%H:%M:%S')

    return _data

  def to_json(self):
    return json.dumps(self.get_data_for_json())

  def to_json_for_html(self):
    return json.dumps(self.get_data_for_json(), cls=JSONEncoderForHTML)

  @property
  def data(self):
    if type(self._data['properties']['start']) != datetime and not '$' in self._data['properties']['start']:
      self._data['properties']['start'] = parse(self._data['properties']['start'])

    if type(self._data['properties']['end']) != datetime and not '$' in self._data['properties']['end']:
      self._data['properties']['end'] = parse(self._data['properties']['end'])

    if self.document is not None:
      self._data['id'] = self.document.id

    return self._data

  @property
  def name(self):
    return self.data['name']

  def set_workspace(self, user):
    self.data['properties']['deployment_dir'] = Job.get_workspace(user)

  @property
  def deployment_dir(self):
    return self.data['properties']['deployment_dir']

  def find_parameters(self):
    params = set()

    for param in find_dollar_braced_variables(self.name):
      params.add(param)

    for param in find_json_parameters([self.data['properties']]):
      params.add(param)

    for param in find_json_parameters(self.data['variables']):
      if param not in ('MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR') and not param.startswith('coord:'):
        params.add(param)

    if self.sla_enabled:
      for param in find_json_parameters(self.sla):
        params.add(param)

    # Get missed params from workflow
    for prop in self.workflow.find_parameters():
      if not prop in params:
        params.add(prop)

    # Remove the ones filled up by coordinator
    removable_names = [ds['workflow_variable'] for ds in self.data['variables']]

    return dict([(param, '') for param in list(params) if param not in removable_names])

  @property
  def sla_enabled(self):
    return self.data['properties']['sla'][0].get('value')

  @property
  def sla(self):
    return self.data['properties']['sla']

  @property
  def parameters(self):
    return self.data['properties']['parameters']

  @property
  def datasets(self):
    return self.inputDatasets + self.outputDatasets

  @property
  def inputDatasets(self):
    return [Dataset(dataset, self) for dataset in self.data['variables'] if dataset['dataset_type'] == 'input_path']

  @property
  def outputDatasets(self):
    return [Dataset(dataset, self) for dataset in self.data['variables'] if dataset['dataset_type'] == 'output_path']

  @property
  def start_server_tz(self):
    return self.data['properties']['start']

  @property
  def end_server_tz(self):
    return self.data['properties']['end']

  @property
  def frequency(self):
    return '${coord:%(unit)s(%(number)d)}' % {'unit': self.data['properties']['frequency_unit'], 'number': self.data['properties']['frequency_number']}

  @property
  def cron_frequency(self):
    data_dict = self.data['properties']

    if 'cron_frequency' in data_dict:
      return data_dict['cron_frequency']
    else:
      # Backward compatibility
      freq = '0 0 * * *'
      if data_dict['frequency_number'] == 1:
        if data_dict['frequency_unit'] == 'minutes':
          freq = '* * * * *'
        elif data_dict['frequency_unit'] == 'hours':
          freq = '0 * * * *'
        elif data_dict['frequency_unit'] == 'days':
          freq = '0 0 * * *'
        elif data_dict['frequency_unit'] == 'months':
          freq = '0 0 0 * *'
      return {'frequency': freq, 'isAdvancedCron': False}

  def to_xml(self, mapping=None):
    if mapping is None:
      mapping = {}

    tmpl = "editor2/gen/coordinator.xml.mako"
    return re.sub(re.compile('\s*\n+', re.MULTILINE), '\n', django_mako.render_to_string(tmpl, {'coord': self, 'mapping': mapping})).encode('utf-8', 'xmlcharrefreplace')

  def clear_workflow_params(self):
    # Repopulated in the config properties
    self.data['variables'] = [dataset for dataset in self.data['variables'] if dataset['dataset_type'] != 'parameter']

  @property
  def properties(self):
    props = [{'name': dataset['workflow_variable'], 'value': dataset['dataset_variable']} for dataset in self.data['variables'] if dataset['dataset_type'] == 'parameter']
    props += self.data['properties']['parameters']
    return props

  @property
  def workflow(self):
    wf_doc = Document2.objects.get(uuid=self.data['properties']['workflow'])
    return Workflow(document=wf_doc)

  def get_absolute_url(self):
    return reverse('oozie:edit_coordinator') + '?coordinator=%s' % self.id

  @classmethod
  def get_application_path_key(cls):
    return 'oozie.coord.application.path'

class Dataset():

  def __init__(self, data, coordinator):
    self._data = data
    self.coordinator = coordinator

  @property
  def data(self):
    self._data['name'] = self._data['workflow_variable']

    return self._data

  @property
  def frequency(self):
    if self.data['same_frequency']:
      if self.coordinator.cron_frequency == '* * * * *':
        frequency_unit = 'minutes'
      elif self.coordinator.cron_frequency == '0 * * * *':
        frequency_unit = 'hours'
      elif self.coordinator.cron_frequency == '0 0 * * *':
        frequency_unit = 'days'
      elif self.coordinator.cron_frequency == '0 0 0 * *':
        frequency_unit = 'months'
      else:
        raise PopupException(_('The frequency of the workflow parameter "%s" cannot be guessed from the frequency of the coordinator.'
                               ' It so needs to be specified manually.') % self.data['name'])
      frequency_number = 1
    else:
      frequency_unit = self.data['frequency_unit']
      frequency_number = self.data['frequency_number']

    return '${coord:%(unit)s(%(number)s)}' % {'unit': frequency_unit, 'number': frequency_number}

  @property
  def start_server_tz(self):
    if self.data['same_start']:
      return self.coordinator.start_server_tz
    else:
      return convert_to_server_timezone(self.data['start'], self.data['timezone'])

  @property
  def timezone(self):
    if self.data['same_timezone']:
      return self.coordinator.data['properties']['timezone']
    else:
      return self.data['timezone']

  @property
  def start_instance(self):
    if not self.is_advanced_start_instance:
      return int(self.data['advanced_start_instance'])
    else:
      return 0

  @property
  def is_advanced_start_instance(self):
    return not self.is_int(self.data['advanced_start_instance'])

  def is_int(self, text):
    try:
      int(text)
      return True
    except ValueError:
      return False

  @property
  def end_instance(self):
    if not self.is_advanced_end_instance:
      return int(self.data['advanced_end_instance'])
    else:
      return 0

  @property
  def is_advanced_end_instance(self):
    return not self.is_int(self.data['advanced_end_instance'])

class Bundle(Job):
  XML_FILE_NAME = 'bundle.xml'
  PROPERTY_APP_PATH = 'oozie.bundle.application.path'
  HUE_ID = 'hue-id-b'

  def __init__(self, data=None, json_data=None, document=None):
    self.document = document

    if document is not None:
      self._data = json.loads(document.data)
    elif json_data is not None:
      self._data = json.loads(json_data)
    elif data is not None:
      self._data = data
    else:
      self._data = {
          'id': None,
          'uuid': None,
          'name': 'My Bundle',
          'coordinators': [],
          'properties': {
              'description': '',
              'deployment_dir': '',
              'schema_version': 'uri:oozie:bundle:0.2',
              'kickoff': datetime.today(),
              'parameters': [{'name': 'oozie.use.system.libpath', 'value': 'true'}]
          }
      }

  @property
  def id(self):
    return self.document.id

  @property
  def uuid(self):
    return self.document.uuid

  def get_data_for_json(self):
    _data = self.data.copy()

    _data['properties']['kickoff'] = _data['properties']['kickoff'].strftime('%Y-%m-%dT%H:%M:%S')

    return _data

  def to_json(self):
    return json.dumps(self.get_data_for_json())

  def to_json_for_html(self):
    return json.dumps(self.get_data_for_json(), cls=JSONEncoderForHTML)

  @property
  def data(self):
    if type(self._data['properties']['kickoff']) == unicode:
      self._data['properties']['kickoff'] = parse(self._data['properties']['kickoff'])

    if self.document is not None:
      self._data['id'] = self.document.id

    return self._data

  def to_xml(self, mapping=None):
    if mapping is None:
      mapping = {}

    mapping.update(dict(list(self.get_coordinator_docs().values('uuid', 'name'))))
    tmpl = "editor2/gen/bundle.xml.mako"
    return force_unicode(
              re.sub(re.compile('\s*\n+', re.MULTILINE), '\n', django_mako.render_to_string(tmpl, {
                'bundle': self,
                'mapping': mapping
           })))

  def get_coordinator_docs(self):
    coordinator_ids = [coordinator['coordinator'] for coordinator in self.data['coordinators']]
    return Document2.objects.filter(type='oozie-coordinator2', uuid__in=coordinator_ids)

  def get_coordinator_objects(self):
    return [Coordinator(document=doc) for doc in self.get_coordinator_docs()]

  @property
  def name(self):
    return self.data['name']

  @property
  def parameters(self):
    return self.data['properties']['parameters']

  @property
  def kick_off_time_utc(self):
    return utc_datetime_format(self.data['properties']['kickoff'])

  def set_workspace(self, user):
    self.data['properties']['deployment_dir'] = Job.get_workspace(user)

  @property
  def deployment_dir(self):
    return self.data['properties']['deployment_dir']

  def find_parameters(self):
    params = set()

    for param in find_dollar_braced_variables(self.name):
      params.add(param)

    for coord in self.get_coordinator_objects():
      params.update(coord.find_parameters())

    for param in find_json_parameters([self.data['properties']]):
      params.add(param)

    # Remove the ones filled up by bundle
    removable_names = [p['name']  for coord in self.data['coordinators'] for p in coord['properties']]

    return dict([(param, '') for param in list(params) if param not in removable_names])

  def get_absolute_url(self):
    return reverse('oozie:edit_bundle') + '?bundle=%s' % self.id

  @classmethod
  def get_application_path_key(cls):
    return 'oozie.bundle.application.path'

class History(object):

  @classmethod
  def get_workflow_from_config(self, conf_dict):
    try:
      doc = Document2.objects.get(type='oozie-workflow2', id=conf_dict.get(Workflow.HUE_ID))
      return Workflow(document=doc)
    except Document2.DoesNotExist:
      pass

  @classmethod
  def get_coordinator_from_config(self, conf_dict):
    try:
      doc = Document2.objects.get(type='oozie-coordinator2', id=conf_dict.get(Coordinator.HUE_ID))
      return Coordinator(document=doc)
    except Document2.DoesNotExist:
      pass

  @classmethod
  def get_bundle_from_config(self, conf_dict):
    try:
      doc = Document2.objects.get(type='oozie-bundle2', id=conf_dict.get(Bundle.HUE_ID))
      return Bundle(document=doc)
    except Document2.DoesNotExist:
      pass

redpajama_set_name: RedPajamaGithub


text:
Senate poised to defy Trump's veto with order to change Confederate base names
A bridge marks the entrance to Fort Benning in Columbus, Ga. Home to the Army's infantry and airborne training programs, the post is named for Confederate Brig. Gen. Henry L. Benning. (John Bazemore/AP)
Karoun Demirjian
National Security reporter focusing on Congress
July 22, 2020 at 10:24 p.m. UTC
The Senate will stand its ground on ordering the Pentagon to rename bases honoring Confederate generals, despite the White House's threat to veto parallel legislation in the House.
The directive, which is part of both the Senate and House versions of a $740 billion military spending bill, topped a list of objections the White House made to the House's legislation Tuesday, arguing it was an "effort to erase from the history of the Nation those who do not meet an ever-shifting standard of conduct." A few hours later, the House passed its version of the defense bill anyway, by a veto-proof majority.
There are 10 Army posts named for Confederate generals. While the House's bill seeks to push the Pentagon to make the name changes within a year, the Senate's bill — which is still under consideration — extends the deadline to three years.
The White House objection made no mention of the timetable. It rejected the notion of any such mandate.
Trump threatens to veto military bill over Confederate-named bases, forcing a showdown with Republicans
The Senate has yet to pass its version of the defense bill. But on Wednesday, it voted to end the debate period for amendments without accommodating a measure from Sen. Josh Hawley (R-Mo.) that sought to strip the legislation's requirement to rename the bases and replace it with a pledge to study the matter instead.
It is unlikely that Hawley's amendment would have secured enough votes to pass, as there is bipartisan support in the Senate for insisting that the Pentagon make the ordered name changes. Yet it is noteworthy that Senate Armed Services Committee Chairman James M. Inhofe (R-Okla.), who has voiced support for preserving the base names, did not try to force the issue.
It is exceedingly possible that the final defense bill — which the House and Senate will have to negotiate over the next several weeks — will include some form of a mandate to strip the Confederate names from military bases, setting up a potential showdown between Congress and the White House.
Republicans signal to Trump to back down on defense bill veto threat over renaming Confederate bases
On other matters, however, it is not yet clear whether Congress will similarly try to call the White House's veto bluff.
The House's bill includes a bipartisan-approved provision to restrict the president's ability to withdraw 9,500 U.S. troops stationed in Germany, as Trump stated last month that he would do. But the Senate ended debate on amendments Wednesday without voting on a similar bipartisan proposal from Sen. Mitt Romney (R-Utah) that had also earned the support of Sens. Lindsey O. Graham (R-S.C.) and Marco Rubio (R-Fla.), both influential GOP voices on foreign policy.
Romney was one of only 13 senators to vote against ending debate on amendments Wednesday, in protest of his amendment not getting a vote — though he said in a statement that he would vote for final passage of the defense bill.
It is still possible that the provision could appear in the final compromise defense bill, however. Inhofe and Senate Armed Services Committee ranking minority-party member Jack Reed (D-R.I.) have expressed personal opposition to the planned troop withdrawal, and the other two principal negotiators — House Armed Services Committee Chairman Adam Smith (D-Wash.) and ranking minority-party member Mac Thornberry (R-Tex.) — voted for the restriction.
Trump approves plan to withdraw one-third of U.S. troops from Germany
It is also still unclear how fiercely Trump will stick to his veto threat. Presidents have often threatened to veto defense bills in the past — former president Barack Obama even made good on one such threat, in 2015, in the midst of a spending dispute. But the annual defense bill has passed Congress for each of the past 59 years.
Smith guessed earlier this month that the defense bill will probably not be completed until November, making the outcome of the 2020 election — and any potential transfers of power in Washington it heralds — a potential factor in 11th-hour negotiations and veto determinations.

redpajama_set_name: RedPajamaCommonCrawl


Why Us? > Why us?
Shekter Dychtenberg LLP is a Toronto-based leading personal injury firm that has earned its reputation for winning precedent-setting cases and setting new benchmarks in damage awards across a wide spectrum of personal injury lawsuits. Our trial lawyers are highly innovative strategic thinkers. Combine those qualities with a depth of experience and specific expertise in: car accident injuries, brain injuries, spinal cord injuries, long term disability claims, slip & fall injuries, medical malpractice, wrongful deaths, paediatric injuries, and other motor vehicle accidents and our clients get the legal minds and results typically associated with larger firms.


One stunning detail can transform the ordinary into a work of art. From birthday celebrations to wedding bells, these pretty in pink sugar peonies cake toppers can turn your simple cake into an extravagant elegant confection.
Add the WOW effect to your cake with these TWO gumpaste flowers. You will receive one 4" sugar peony and 2 1/2 inch smaller peony. These handmade cake decorations are white with pink dusting on the tips, but other colors are available upon request. No one needs to know how affordable or easy it was to transform your stunning cake with these precious pink peonies sugar flowers.
Measurements: 4" and 2 1/2"
Additional Notes: Listing is for Two sugar flowers.


text:
Senior students selected by the School of Art and College of Architecture faculty will present their work in the Ewing Gallery.
Exhibiting students from the School of Art are: Thais Lloyd, Tammy Castany, and Jace Hermanto.
Exhibiting students from the College of Architecture and Design are: Marilyn Reish, Kristin Pitts, Riley Doty, Madeleine Smith, Phillip Minton, and Raven Carrier.
Lilly Saywitz, Mary Climes, Jake Miller and Alissa Walls
Lauren Terry, Conor McGrann, Gina Stucchio and Nyasha Madamombe
Erin Wohletz, Quynh Nguyen, and Kate Clark
Quynh Lam
Nuroc A Memoir
Kate Clark
Bunker Bunker Crafting Hour
Erin Wohletz
The Black Vortex
Lauren Terry
Macaroni Titans
Gina Stucchio
Parasite with a Proper Name
Nyasha Madamombe
NDEZVEDZINZA
Alissa Walls The Space Race
Alissa Walls
Conor McGrann It's not Adding Up
Conor McGrann
It's not Adding Up
In Your Way - Kate Gilmore
Gilmore's work incorporates performance, video, sculpture, and painting. In Your Way features ten works—nine performance-based videos and one live performance/sculptural installation that invites audience participation. Gilmore's videos focus on herself or several women, wearing stereotypical feminine clothing and footwear while persistently performing difficult, labor-intensive tasks within self-constructed spaces.
In her videos, Gilmore critiques and also inserts herself into male dominated movements such as Abstract Expressionism and Minimalism, exploring feminist themes and modern and contemporary art tropes, all the while exhibiting relentless determination. The spilling and splattering from her work are an ode to Abstract Expressionism or 1950s stripe paintings.
Her works are mischievous and political, as well as humorous and critical of the heroic language and absence of women in these artistic movements. The physical situations and actions Gilmore creates for herself and her performers are metaphors for challenges women face culturally and socially.
November 15, 2018 - February 20, 2019Curated by: T. Michael MartinMutual Muses is a two-person exhibition showcasing works by James Seawright and Mimi Garrard, who have been working together as well as individually since the 1960s. Their lives and practice have inspired each other throughout their careers. This exhibition is an interwoven love story featuring individual works by Seawright and Garrard as well as ones inspired by the other and those created collaboratively. Their life of interconnectivity as mutual muses is beautifully explored and presented in this survey exhibition.
July 18 - September 7, 2018
Marrying Mr. Darcy
Marrying Mr. Darcy is a strategy card game for 2-6 players based on Jane Austen's classic novel "Pride and Prejudice."
Walden, a gamePlay as Henry David Thoreau, the American philosopher and naturalist, during his experiment in living simply in nature. Apply yourself to both the daily tasks of maintaining your basic needs of life at Walden Pond, as well as searching for the small beauties and wonders of nature in this virtual environment.
Embodied Labs
Embodied Labs works at the intersection of healthcare training and virtual reality storytelling to provide a culture shift solution that empowers every member of the care team to share their expertise and value one another.
The Cat and the Coup
The Cat and the Coup is a documentary videogame in which you play the cat of Dr. Mohammed Mossadegh, the first democratically elected Prime Minister of Iran. During the summer of 1953, the CIA engineered a coup to bring about his downfall. As a player, you coax Mossadegh back through significant events of his life by knocking objects off of shelves, scattering his papers, jumping on his lap and scratching him.
Good Society is a collaborative roleplaying game that seeks to capture the heart, and the countenance, of Jane Austen's work. It is a game of balls, estates, sly glances, and turns about the garden. At least on the surface. Underneath this, just as in Austen's own novels, it is a game of social ambition, family obligation and breathtaking, heart-stopping longing.


Zone Striping owns and operates the industry's most noted brand of Hydro-Milling (also known as Water-Blasting) equipment. This equipment is not only the most production oriented but the most versatile units currently available. The equipment is able to remove highway long-line and short-line markings as well as runway and taxiway markings. Its self contained water recovery system not only makes the process one simple step, but also is environmentally friendly as it is able to recycle the water while separating the removal debris.