Editorial Workflows

RV: Workflow Installer

public workflow

Install Workflow...

This workflow contains at least one Python script. Only use it if you trust the person who shared this with you, and if you know exactly what it does.

I understand, install the workflow!

This is a workflow for Editorial, a Markdown and plain text editor for iOS. To download it, you need to view this page on a device that has the app installed.

Description: Workflow installer allows you to browse, search and install workflows. Workflow can reference another workflow by name, but this referenced workflow is not installed by default and you end up with error if you try to run main workflow. Use this one to install workflows with dependencies (= referenced workflows) as well.

Sharing state is also preserved when installing workflow on a device from which you did share it. In other words, you can create workflow, share it, delete it, install it and send updates to editorial-workflows.com.

Compatibility: Tested with Editorial 1.1.1 only.

More info at: http://www.robertvojta.pro/2015/05/16/editorial-workflow-installer/

Shared by: @robertvojta

Comments: Comment Feed (RSS)

@robertvojta — 15 May 2015
Do not install now. There's a problem with sub workflow installations. I have a fixed version, but can't update this workflow :( See http://omz-forums.appspot.com/editorial/post/5800582476464128.
@robertvojta — 15 May 2015
Fixed, thanks to @omz. I can update my workflows again. Feel free to install it.
@robertvojta — 16 May 2015
2015-05-16 Update

* direct workflow installation (no editorial:// url scheme)
* dependencies installation
* sharing state preserved
@robertvojta — 20 May 2015
2015-05-20 Update

* detail button added to check workflow description

+ Add Comment

Workflow Preview
Run Python Script ?
Source Code
#coding: utf-8
import workflow
import console
import editor
import json
import requests
import os
import uuid
import shutil
import ui
import urllib
import webbrowser
import sqlite3
import sys
import urlparse

#
# Local Workflows Actions
#
#  - workflows directory editor.get_workflows_path()
#  - actions of each workflow stored in '.wkflw' file, root key 'actions' (JSON)
#  - workflow filename is generated UUID version 4, keep it to avoid conflicts
#
# List of Local Workflows
#
#  - 'Commands.edcmd' file (JSON) in editor.get_workflows_path() directory
#  - top level array of workflows
#  - sample command (= workflow info):
#
#    {
#      "filename" : "2C5A729C-...E334.wkflw",
#      "title" : "Level 2",
#      "uuid" : "2C5A729C-...E334"       
#    }
#
# Editorial Workflows API
#
#  - http://www.editorial-workflows.com/workflows/api
#

#
# Workflow helpers
#
def __dependencies(deps,obj):
	# TODO Make it nicer, just a quick mess
	if isinstance(obj, list):
		for o in obj:
			__dependencies(deps, o)
		return
			
	if not isinstance(obj, dict):
		return

	if 'class' in obj:			
		if obj['class'] == 'WorkflowActionRunSubWorkflow':
			workflow = obj['parameters']['workflow']
			if not 'data' in workflow:
				deps.add(workflow['name'])
				return
			
	for (key, value) in obj.iteritems():
		__dependencies(deps, value)
		
#
# Sequence of dependencies. Name of workflows referenced by name.
#		
def workflow_dependencies(workflow):
	data = workflow.get('workflow_data', None)
	if not data:
		return None
		
	actions = data.get('actions', None)
	if not actions:
		return []
						
	deps = set([])
	__dependencies(deps, data)
	return list(deps)

#
# Remote Workflows
#
def fetch_workflow_info(url):
	console.show_activity()	
	response = requests.get(url, params={'format':'json'})
	workflow = None
	try:
		workflow = response.json()
	except:
		pass
	console.hide_activity()
	return workflow

def fetch_recent_workflows(limit=50):
	console.show_activity()
	params = { 'format' : 'json' }
	response = requests.get('http://editorial-app.appspot.com/workflows/recent', params=params)
	workflows = []
	try:
		workflows = response.json()['workflows']
	except:
		pass
	console.hide_activity()
	return workflows

def fetch_workflows_containing(query):
	console.show_activity()
	
	params = {
		'format' : 'json',
		'q' : '"%s"' % query          
	}
	
	response = requests.get('http://editorial-app.appspot.com/workflows/search', params=params)
	workflows = []
	try:
		workflows = response.json()['workflows']
	except:
		pass
	console.hide_activity()
	return workflows
	
def fetch_workflow_named(name):
	workflow = None
	console.show_activity()
	try:
		workflows = fetch_workflows_containing(name)
		url = None
		for w in workflows:
			if w['title'] == name:
				url = w['url']
				break
				
		if url:
			workflow = fetch_workflow_info(url)
				
	except:
		pass
		
	console.hide_activity()
	return workflow
	
#
# Local Workflows
#
def load_installed_workflows():
	filename = os.path.abspath(os.path.join(editor.get_workflows_path(), 'Commands.edcmd'))
	workflows = None
	with open(filename, 'r') as file:
		workflows = json.load(file)
	return workflows
							
def save_installed_workflows(workflows):
	filename = os.path.abspath(os.path.join(editor.get_workflows_path(), 'Commands.edcmd'))
	with open(filename, 'w') as file:
		json.dump(workflows, file)

def install_workflows(workflows):
	console.set_idle_timer_disabled(True)
	print 'Installing %d workflows' % len(workflows)
	
	error = None
		
	db_path = os.path.abspath(os.path.join(editor.get_workflows_path(), '../SharedWorkflows.sqlite'))
	db_conn = None
	db_cursor = None
	if os.path.exists(db_path):
		db_conn = sqlite3.connect(db_path)
		db_cursor = db_conn.cursor()

	installed_commands = load_installed_workflows()
	new_commands = []
		
	try:
		for workflow in workflows:
			print '\n%s' % workflow['title']
			
			if not 'workflow_data' in workflow:
				error = 'Invalid workflow JSON - missing workflow_data or title'
				break
			
			data = workflow['workflow_data']	

			url = None
			parsed_url = urlparse.urlparse(workflow['url'])
			if parsed_url.path:
				url = 'http://www.editorial-workflows.com%s' % parsed_url.path

			if db_cursor and url:
				db_cursor.execute('SELECT uuid FROM workflows WHERE url == ?', (url,))
				db_row = db_cursor.fetchone()
			if db_row:
				identifier = db_row[0]
				print 'Reusing old uuid %s (able to update workflow)' % identifier
			else:
				identifier = str(uuid.uuid4())
				print 'New uuid generated %s' % identifier

			filename = '%s.wkflw' % identifier			
			path = os.path.abspath(os.path.join(editor.get_workflows_path(), filename))
			with open(path, 'w') as file:
				json.dump(data, file)
			print 'Stored in filename %s' % filename
				
			command = {}
			command['title'] = workflow['title']
			command['filename'] = filename
			command['uuid'] = identifier
			if 'icon' in data:
				command['iconName'] = data['icon']
			if 'description' in data:
				command['comment'] = data['description']
			new_commands.append(command)
#			print 'Workflow command:'
#			print command
											
	except:
		error = sys.exc_info()[0]

	if db_conn:
		db_conn.close()

	if not error and len(new_commands) > 0:
		print '\nSaving new workflow entries'
		commands = new_commands + installed_commands
		save_installed_workflows(commands)
		editor.reload_workflows()
		print 'Editorial workflows reloaded'
		
			
	if error:
		print '\nInstallation failed, removing installed workflows ...'
		for workflow in new_commands:
			filename = workflow['filename']
			path = os.path.abspath(os.path.join(editor.get_workflows_path(), filename))
			if os.path.exists(filename):
				os.remove(filename)
				print '%s removed' % filename
	
	console.set_idle_timer_disabled(False)
	if error:
		print '\nFailed with error: %s' % error
		console.hud_alert('Failed to install workflows', 'error')
	else:
		console.hud_alert('Workflows installed')
							
#
# Rect tuples manipulation
#
def rect_min_x(rect):
	return rect[0]
	
def rect_min_y(rect):
	return rect[1]
	
def rect_width(rect):
	return rect[2]
	
def rect_height(rect):
	return rect[3]
	
def rect_max_x(rect):
	return rect_min_x(rect) + rect_width(rect) - 1
	
def rect_max_y(rect):
	return rect_min_y(rect) + rect_height(rect) - 1

def rect_move_y(rect,offset,modify_height=True):
	result = list(rect)
	result[1] = rect[1] + offset
	if modify_height:
		result[3] = rect[3] - offset
	return tuple(result)
	
#
# UI constants
#
SPACING = 6

#
# Workflow browser UI
#		
class WorkflowBrowserView(ui.View):
	def __init__(self):
		self.name = 'Workflows'
		self.background_color = 'white'
		self.workflows = []
		self.header_title = None		
		self.ctx = {}
				
		cancel_item = ui.ButtonItem(title='Cancel',action=self.cancel)
		self.left_button_items = [cancel_item]
				
		self.search_field = ui.TextField()
		self.search_field.frame = ( 40, SPACING, rect_width(self.bounds) - 2 * SPACING - 34, 32 )
		self.search_field.placeholder = 'Search editorial-workflows.com ...'
		self.search_field.flex = 'W'
		self.search_field.delegate = self
		self.add_subview(self.search_field)
		
		self.tableview = ui.TableView()
		self.tableview.flex = 'WHB'
		self.tableview.frame = rect_move_y(self.bounds, rect_max_y(self.search_field.frame) + 1 + SPACING)
		self.tableview.data_source = self
		self.tableview.delegate = self
		self.add_subview(self.tableview)		

		self.activity_indicator = ui.ActivityIndicator()
		self.activity_indicator.hides_when_stopped = False
		self.activity_indicator.style = ui.ACTIVITY_INDICATOR_STYLE_GRAY
		self.activity_indicator.center = (21,21)
		self.activity_indicator.flex = ''
		self.add_subview(self.activity_indicator)
		
		self.schedule_reload(0.1)
		
	def cancel(self,sender):
		self.navigation_view.close()
		
	def will_close(self):
		self.cancel_scheduled_reload()
		
	def cancel_scheduled_reload(self):
		ui.cancel_delays()
		
	def schedule_reload(self, interval=0.5):
		self.cancel_scheduled_reload()
		ui.delay(self.reload_workflows, interval)
		
	#
	# Load recent workflows (if search field is empty) or load workflows
	# conforming to search query
	#
	@ui.in_background
	def reload_workflows(self):
		self.activity_indicator.start()
		query = self.search_field.text.strip()
		if len(query) > 0:
			self.workflows = fetch_workflows_containing(query)
			self.header_title = 'Search \'%s\'' % query
		else:
			self.workflows = fetch_recent_workflows()
			self.header_title = 'Recent Workflows'
		self.tableview.reload_data()
		self.activity_indicator.stop()
	
	#
	# TextField delegate
	#
	def textfield_should_change(self, textfield, range, replacement):
		self.schedule_reload()
		return True
	
	#
	# TableView data source
	#
	def tableview_title_for_header(self, tableview, section):	
		return self.header_title
		
	def tableview_number_of_sections(self, tableview):
		return 1

	def tableview_number_of_rows(self, tableview, section):
		return len(self.workflows)

	def tableview_cell_for_row(self, tableview, section, row):
		workflow = self.workflows[row]
		
		cell = ui.TableViewCell('value1')
		cell.text_label.text = workflow['title']
		cell.detail_text_label.text = ''
		if workflow.get('description', None):
			cell.accessory_type = 'detail_disclosure_button'
		else:
			cell.accessory_type = 'disclosure_indicator'
		
		return cell
		
	#
	# TableView delegate
	#
	def tableview_did_select(self, tableview, section, row):					
		detail = WorkflowDetailView(self.workflows[row], self.ctx)
		
		self.navigation_view.push_view(detail)
		tableview.selected_row = -1
		
	def tableview_accessory_button_tapped(self, tableview, section, row):
		workflow = self.workflows[row]
		view = WorkflowDescriptionView(workflow['description'])
		view.name = workflow['title']
		self.navigation_view.push_view(view)

#
# Workflow Description
#
class WorkflowDescriptionView(ui.View):
	def __init__(self, description):
		self.textview = ui.TextView()
		self.textview.frame = self.bounds
		self.textview.flex = 'WH'
		self.textview.editable = False
		self.textview.text = description
		self.add_subview(self.textview)
		
#
# Workflow detail
#
class WorkflowDetailView(ui.View):
	def __init__(self,workflow,ctx):
		self.name = 'Status'
		self.background_color = 'white'
		self.ctx = ctx
		
		self.activity_label = ui.Label()
		self.activity_label.text = 'Gathering workflow info ...'
		self.activity_label.flex = 'WB'
		self.activity_label.frame = ( 6, 20, self.bounds[3] - 12, 32 )
		self.activity_label.alignment = ui.ALIGN_CENTER
		self.activity_label.font = ( '<system>', 15 )
		self.add_subview(self.activity_label)

		self.activity = ui.ActivityIndicator(ui.ACTIVITY_INDICATOR_STYLE_GRAY)
		self.activity.hides_when_stopped = True
		self.activity.flex = 'LRB'
		self.activity.center = (self.center[0], 70)
		self.add_subview(self.activity)
				
		self.activity.start()
		
		self.gather_workflow_info(workflow)
		
	@ui.in_background
	def gather_workflow_info(self, workflow):
		if not 'workflow_data' in workflow:
			self.activity_label.text = 'Fetching %s info ...' % workflow['title']
			workflow = fetch_workflow_info(workflow['url'])
			if not workflow:
				console.hud_alert('Failed to fetch workflow info', 'error')
				self.navigation_view.pop_view()
				return
				
		self.workflow = workflow
			
		installed_cache	= self.ctx.get('installed_cache', None)
		if not installed_cache:
			installed_cache = {}
			self.ctx['installed_cache'] = installed_cache

		installed_workflows = load_installed_workflows()
		workflows_to_install = []
		
		meta_ready_to_install = {
			'status' : 'Ready to install',
			'status_color' : 'green'
		}

		meta_already_installed = {
			'status' : 'Already installed',
			'status_color' : 'gray'
		}
				
		w = next((x for x in installed_workflows if x['title'] == workflow['title']), None)
		if not w:
			workflow['__meta'] = meta_ready_to_install
			workflows_to_install.append(workflow)
		else:
			workflow['__meta'] = meta_already_installed
		
		dependency_names = workflow_dependencies(workflow)
		dependencies = []
		for name in dependency_names:
			w = next((x for x in installed_workflows if x['title'] == name), None)
			if w:
				# Workflow installed
				workflow = {
				  'title' : name,
				  '__meta' : meta_already_installed
				}
				dependencies.append(workflow)
			else:
				self.activity_label.text = 'Fetching %s info ...' % name
				workflow = fetch_workflow_named(name)
				if workflow:
					workflow['__meta'] = meta_ready_to_install
					dependencies.append(workflow)
					workflows_to_install.append(workflow)
				else:					
					console.hud_alert('Failed to fetch workflow info', 'error')
					self.navigation_view.pop_view()
					return

		self.dependencies = dependencies
		self.workflows_to_install = workflows_to_install
				
		self.hide_activity()

	def hide_activity(self):
		self.tableview = ui.TableView()
		self.tableview.data_source = self
		self.tableview.flex = 'WH'
		self.tableview.frame = self.bounds
		self.tableview.allows_selection = False
		self.add_subview(self.tableview)
		
		self.activity.stop()
		self.activity_label.hidden = True
		
		self.tableview.reload()
		
		if len(self.workflows_to_install):
			install_item = ui.ButtonItem(title='Install', action=self.install)
			self.right_button_items = [ install_item ]
		
	#
	# User actions
	#		
	@ui.in_background
	def install(self,sender):
		button = console.alert('Warning', 'Selected workflow(s) can contain Python script. Only use if you trust the person who shared this with you, and if you know exactly what it does.', 'Cancel', 'Continue', hide_cancel_button=True)
		if button == 0:
			return
		self.navigation_view.close()
		for workflow in self.workflows_to_install:
			del workflow['__meta']
		install_workflows(self.workflows_to_install)
		
	#
	# TableView data source
	#
	def tableview_title_for_header(self, tableview, section):		
		if section == 1:
			return 'Dependencies'
		return None

	def tableview_number_of_sections(self, tableview):
		return 2

	def tableview_number_of_rows(self, tableview, section):
		if section == 0:
			return 1						
		if len(self.dependencies) == 0:
			return 1						
		return len(self.dependencies)
		
	def tableview_cell_for_row(self, tableview, section, row):
		if section == 0:
			obj = self.workflow
		if section == 1:
			if len(self.dependencies) == 0:
				cell = ui.TableViewCell()
				cell.text_label.text = 'No dependencies'
				cell.text_label.text_color = 'gray'
				return cell
			obj = self.dependencies[row]

		meta = obj['__meta']			
		cell = ui.TableViewCell('subtitle')						
		cell.text_label.text = obj['title']
		cell.detail_text_label.text = meta['status']
		cell.detail_text_label.text_color = meta['status_color']
		
		return cell
					
#
# Main
#				
browser = WorkflowBrowserView()
navigation = ui.NavigationView(browser)
navigation.present(style='sheet', hide_title_bar=True)