Skip to content

Instantly share code, notes, and snippets.

@hiltmon
Created March 26, 2014 15:41
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save hiltmon/9786291 to your computer and use it in GitHub Desktop.
Save hiltmon/9786291 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# merge_asana_into_omnifocus.rb
# Hilton Lipschitz
# http://www.hiltmon.com
# Use and modify freely, attribution appreciated
# Script to import Asana projects and their tasks into
# OmniFocus and keep them up to date from Asana.
require "rubygems"
require "JSON"
require "net/https"
class MergeAsanaIntoOmnifocus
API_KEY = '***'
ASIGNEE_NAME = '***'
SUBTASKS = true
def get_json_data(url_string)
# set up HTTPS connection
uri = URI.parse(url_string)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
# set up the request
header = {
"Content-Type" => "application/json"
}
req = Net::HTTP::Get.new(uri, header)
req.basic_auth(API_KEY, '')
# issue the request
res = http.start { |http| http.request(req) }
# Parse the result
body = JSON.parse(res.body)
if body['errors'] then
puts "Server returned an error: #{body['errors'][0]['message']}"
return nil
end
body
end
# -----------------------------------------------------------------------
# ASANA API Calls
# -----------------------------------------------------------------------
def get_projects
body = get_json_data("https://app.asana.com/api/1.0/projects")
projects = {}
body["data"].each do |element|
projects[element["id"]] = element["name"].gsub("'", '').gsub("\n", '')
end
projects
end
# NOTE: Assignee = me is ignored in projects filter
def get_tasks_in_project(project_id)
body = get_json_data("https://app.asana.com/api/1.0/tasks?project=#{project_id}&assignee=me")
tasks = {}
body["data"].each do |element|
tasks[element["id"]] = element["name"].gsub("'", '').gsub("\n", '')
end
tasks
end
def get_subtasks_in_task(task_id)
body = get_json_data("https://app.asana.com/api/1.0/tasks/#{task_id}/subtasks")
body["data"]
end
def get_project_detail(project_id)
body = get_json_data("https://app.asana.com/api/1.0/projects/#{project_id}")
body["data"]
end
def get_task_detail(task_id)
body = get_json_data("https://app.asana.com/api/1.0/tasks/#{task_id}")
body["data"]
end
# -----------------------------------------------------------------------
# AppleScripts
# -----------------------------------------------------------------------
def project_exists_osascript(project_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
try
set myProject to project "#{project_name}" in folder "Asana"
on error
-- Project is Missing, return failure code
return "Missing"
end try
return "Found"
end tell
end tell
}
end
# Note the hack to escape the single quote in Applescript's
def my_tasks_osascript(project_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set myProject to project "#{project_name}" in folder "Asana"
set myRowNames to name of every task of myProject
set AppleScript'"'"'s text item delimiters to "|"
set retVal to myRowNames as string
return retVal
end tell
end tell
}
end
def project_osascript(project_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
try
set myProject to project "#{project_name}" in folder "Asana"
on error
-- Project is Missing, create it
set myProject to make new project with properties {name: "#{project_name}"} at end of projects of myFolder
end try
end tell
end tell
}
end
def project_task_osascript(project_name, task_name, note, completed, due_date, context)
if due_date.nil?
due_date_str = '"none"'
else
due_date_str = "date \"#{due_date}\""
end
completed_str = (completed ? 'true' : 'false')
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set myProject to project "#{project_name}" in folder "Asana"
set isCompleted to #{completed_str}
set dueDate to #{due_date_str}
set newContext to "#{context}"
if (newContext ≠ "none") then
set parentContext to context "People"
try
set myContext to first flattened context whose name is newContext
on error
-- Context is Missing, create it
tell parentContext
set myContext to make new context with properties {name:newContext}
end tell
end try
end if
try
set myTask to task "#{task_name}" in myProject
on error
-- Task is Missing, create it unless completed?
if (isCompleted = false) then
set myTask to make new task with properties {name:"#{task_name}"} at end of tasks of myProject
end if
end try
-- At this point, myTask exists if not completed
-- Since AppleScript has no test of undefined, wrap the next block
try
if (isCompleted = true) then
-- if it exists and is completed
tell myTask
set its completed to true
end tell
else
-- Update its notes and dates
tell myTask
set its note to "#{note}"
if (newContext ≠ "none") then
set its context to myContext
end if
if (dueDate ≠ "none") then
set its due date to dueDate
end if
end tell
end if
on error
-- No, just the task is complete, no clutter in OmniFocus
end try
end tell
end tell
}
end
def my_task_complete_osascript(project_name, task_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set myProject to project "#{project_name}" in folder "Asana"
set isCompleted to true
try
set myTask to task "#{task_name}" in myProject
tell myTask
set its completed to true
end tell
on error
-- Task is Missing, ignore it
end try
end tell
end tell
}
end
# Note the hack to escape the single quote in Applescript's
def my_subtasks_osascript(project_name, parent_task_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana"
set myRowNames to name of every task of parentTask
set AppleScript'"'"'s text item delimiters to "|"
set retVal to myRowNames as string
return retVal
end tell
end tell
}
end
def project_sub_task_osascript(project_name, parent_task_name, task_name, note, completed, due_date, context)
if due_date.nil?
due_date_str = '"none"'
else
due_date_str = "date \"#{due_date}\""
end
completed_str = (completed ? 'true' : 'false')
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana"
set isCompleted to #{completed_str}
set dueDate to #{due_date_str}
set newContext to "#{context}"
if (newContext ≠ "none") then
set parentContext to context "People"
try
set myContext to first flattened context whose name is newContext
on error
-- Context is Missing, create it
tell parentContext
set myContext to make new context with properties {name:newContext}
end tell
end try
end if
try
set myTask to task "#{task_name}" in parentTask
on error
-- Task is Missing, create it unless completed?
if (isCompleted = false) then
set myTask to make new task with properties {name:"#{task_name}"} at end of tasks of parentTask
end if
end try
-- At this point, myTask exists if not completed
-- Since AppleScript has no test of undefined, wrap the next block
try
if (isCompleted = true) then
-- if it exists and is completed
tell myTask
set its completed to true
end tell
else
-- Update its notes and dates
tell myTask
set its note to "#{note}"
if (newContext ≠ "none") then
set its context to myContext
end if
if (dueDate ≠ "none") then
set its due date to dueDate
end if
end tell
end if
on error
-- No, just the task is complete, no clutter in OmniFocus
end try
end tell
end tell
}
end
def my_sub_task_complete_osascript(project_name, parent_task_name, sub_task_name)
%Q{
tell application "OmniFocus"
tell front document
set myFolder to folder "Asana"
set parentTask to task "#{parent_task_name}" in project "#{project_name}" in folder "Asana"
set isCompleted to true
try
set myTask to task "#{sub_task_name}" in parentTask
tell myTask
set its completed to true
end tell
on error
-- Task is Missing, ignore it
end try
end tell
end tell
}
end
# -----------------------------------------------------------------------
# MAIN LOOP
# -----------------------------------------------------------------------
def run
projects = get_projects
projects.each_pair do |project_id, project_name|
detail = get_project_detail(project_id)
# Skip archived projects if not in Omnifocus
if detail['archived'] == true
result = %x{osascript -e '#{project_exists_osascript(project_name)}'}
# puts "#{project_name}: [#{result}]"
if result == 'Missing'
puts "Skipping Archived Project: #{project_name}..."
next
end
end
# Create or update the project in OmniFocus using AppleScript
%x{osascript -e '#{project_osascript(project_name)}'}
# Project / Tasks
puts "Tasks for #{project_name}..."
tasks = get_tasks_in_project(project_id)
# Get my known tasks
result = %x{osascript -e '#{my_tasks_osascript(project_name)}'}
my_known_tasks = result.strip.split('|')
tasks.each_pair do |task_id, task_name|
next if task_name == "" # We seem to have these!
detail = get_task_detail(task_id)
# puts detail if project_name == 'Single cusip view'
# Create or update a task in a project
notes = detail['notes'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '')
task_name = task_name.gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '')
context = 'none'
unless detail['assignee'].nil?
if detail['assignee']['name'] != ASIGNEE_NAME
context = detail['assignee']['name']
end
end
%x{osascript -e '#{project_task_osascript(project_name, task_name, notes, detail['completed'], detail['due_on'], context)}'}
# Remove from my known as we see it in Asana
# puts "///DEL/// [#{task_name}]"
my_known_tasks.delete(task_name)
next if detail['completed'] == true # Ignore subtasks
if SUBTASKS == true
# Project / Task / SubTask
puts " Subtasks for #{task_name}..."
subtasks = get_subtasks_in_task(task_id)
# Get my known sub-tasks
result = %x{osascript -e '#{my_subtasks_osascript(project_name, task_name)}'}
my_known_sub_tasks = result.strip.split('|')
if subtasks.length > 0
subtasks.each do |sub_task|
next if sub_task["name"] == ""
sub_detail = get_task_detail(sub_task["id"])
sub_context = context
unless detail['assignee'].nil?
if detail['assignee']['name'] != ASIGNEE_NAME
sub_context = detail['assignee']['name']
end
end
# Create or update a subtask in Omnifocus
sub_notes = sub_detail['notes'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '')
sub_task_name = sub_task['name'].gsub("'", '').gsub('"', '').gsub('`', '').gsub("\n", '')
%x{osascript -e '#{project_sub_task_osascript(project_name, task_name, sub_task_name, sub_notes, sub_detail['completed'], sub_detail['due_on'], sub_context)}'}
# Remove from my known as we see it in Asana
# puts "///DEL/// #{task_name}"
my_known_sub_tasks.delete(sub_task_name)
end
end
my_known_sub_tasks.each do |sub_task_name|
sub_task_name.strip!
next if sub_task_name == "" # We seem to have these!
puts "Missing: #{sub_task_name} under #{task_name} // Marked as completed"
%x{osascript -e '#{my_sub_task_complete_osascript(project_name, task_name, sub_task_name)}'}
end
end
end
my_known_tasks.each do |task_name|
task_name.strip!
next if task_name == "" # We seem to have these!
puts "Missing: #{task_name} // Marked as completed"
%x{osascript -e '#{my_task_complete_osascript(project_name, task_name)}'}
end
end
end
def test
project_name = 'Single cusip view'
result = %x{osascript -e '#{my_tasks_osascript(project_name)}'}
puts result.split('|')
end
end
app = MergeAsanaIntoOmnifocus.new()
app.run()
# app.test()
@huyz
Copy link

huyz commented Aug 15, 2014

Thanks for that.

A couple of fixes in my fork: https://gist.github.com/huyz/9caa5e95b7a1f6f8f356

@abolishme
Copy link

hey this works great on OmniFocus 2.0.4 (v87.19), pro edition. it should be noted somewhere, perhaps in comments at the top of this script that you need a folder called "Asana" in your projects area for this to work.

@GolfRomeo
Copy link

Awsome, it does work fine with OmniFocus Pro 2.1 too! Many thanks! (using huyz script). But I am too stupid to understand the time format (no idea of coding) - the script does give me errors on the time & date format - using a non US time format on the computer. If I set the time format to US, works like a charme.
Could someone explain to me what I need to change exactly in the script to make it work with Eu or Australian settings? Unfortunately there is no way to change Asana to those settings. many thanks again!

@squarepegsys
Copy link

FYI -- I had to make some minor changes (really from the Asana side) but it still works in OmniFocus 3
my fork is here:
https://gist.github.com/squarepegsys/8e9dd706773c2b2e480b9e78b93b52e1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment