harvestthingstest 1.0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ lib/harvestthings/harvest/config.rb
3
+ harvestthings*.gem
data/README.mdown ADDED
@@ -0,0 +1,78 @@
1
+ HarvestThings
2
+ =============
3
+
4
+
5
+ HarvestThings is a Ruby Gem that syncs clients, projects, and tasks from
6
+ Things for Mac and pushed them into Harvest–the best time tracking utility out
7
+ there.
8
+
9
+ To learn more about Things:
10
+ [http://culturedcode.com/things/][]
11
+
12
+ To learn more about Harvest:
13
+ [http://www.getharvest.com/][]
14
+
15
+
16
+ The source for this gem is located on Github:
17
+ [http://github.com/mkrisher/harvestthings/][]
18
+
19
+ The gem can be installed from gemcutter using:
20
+ `gem install harvestthings`
21
+
22
+ Details
23
+ =======
24
+
25
+ Harvest is an amazing web based time tracking utility from Iridesco. They
26
+ offer a clean Web interface, a Dashboard widget, and an API. However, I didn't
27
+ want to have to retype all of my clients, projects, and tasks twice. I was
28
+ already keeping track of all of these items in Things for Mac. And when
29
+ recording time, I want to record against the actual task from Things that I
30
+ was working on. So, this gem was created as a way to take the project and tasks
31
+ from Things and push them into Harvest via the API.
32
+
33
+ A typical workflow then becomes. Task gets created in Things and assigned to a
34
+ project. That project belongs to an area of responsibility. Syncing to Harvest
35
+ can become really easy. This gem assumes that "Areas of Responsibility" in
36
+ Things represent "clients" in Harvest. Projects in Things are projects in
37
+ Harvest. Tasks belong to projects whether in Things or Harvest. The image
38
+ below shows how these three items match up.
39
+
40
+ [![](http://img.skitch.com/20091125-jptpbxfbcg4irp81ytnwf3fkxf.jpg)](http://img.skitch.com/20091125-jptpbxfbcg4irp81ytnwf3fkxf.jpg)
41
+
42
+
43
+ Requirements
44
+ =======
45
+
46
+ The HarvestThings Ruby Gem requires a few other gems and libraries in order to make
47
+ the API calls (other gems are not listed as dependents):
48
+
49
+ * hpricot
50
+ * net/http
51
+ * net/http
52
+ * uri
53
+ * base64
54
+ * bigdecimal
55
+ * date
56
+ * time
57
+ * jcode
58
+
59
+ Usage
60
+ =====
61
+ as a Ruby Gem
62
+
63
+
64
+ rquire rubygems
65
+ require harvestthings
66
+ harvestthings
67
+
68
+
69
+ TODO
70
+ ====
71
+ * add tests
72
+
73
+
74
+ Copyright (c) 2009 Michael Krisher, released under the MIT license
75
+
76
+ [http://culturedcode.com/things/]: http://culturedcode.com/things/
77
+ [http://www.getharvest.com/]: http://www.getharvest.com/
78
+ [http://github.com/mkrisher/harvestthings/]: http://github.com/mkrisher/harvestthings/
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gemspec|
8
+ gemspec.name = "harvestthingstest"
9
+ gemspec.summary = "sync projects and tasks between Things and Harvest"
10
+ gemspec.description = "harvestthings will sync your clients, projects, and tasks between Things and Harvest, where areas in Things correspond to clients in Harvest"
11
+ gemspec.email = "barry@bjhess.com"
12
+ gemspec.homepage = "http://github.com/bjhess/HarvestThings"
13
+ gemspec.authors = ["Barry Hess"]
14
+ gemspec.add_dependency('hpricot', '>= 0.8.1')
15
+ gemspec.add_dependency('net/http')
16
+ gemspec.add_dependency('jcode')
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
21
+
22
+ Jeweler::GemcutterTasks.new
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ =begin
2
+ TODO add redundant checking, allowing same project name for two clients in Harvest
3
+ =end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 0
4
+ :patch: 0
5
+ :build: 1
@@ -0,0 +1,61 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{harvestthingstest}
8
+ s.version = "1.0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Barry Hess"]
12
+ s.date = %q{2009-12-11}
13
+ s.description = %q{harvestthings will sync your clients, projects, and tasks between Things and Harvest, where areas in Things correspond to clients in Harvest}
14
+ s.email = %q{barry@bjhess.com}
15
+ s.extra_rdoc_files = [
16
+ "README.mdown",
17
+ "TODO"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "README.mdown",
22
+ "Rakefile",
23
+ "TODO",
24
+ "VERSION.yml",
25
+ "harvestthingstest.gemspec",
26
+ "lib/harvestthings.rb",
27
+ "lib/harvestthings/application.rb",
28
+ "lib/harvestthings/harvest.rb",
29
+ "lib/harvestthings/sync.rb",
30
+ "lib/harvestthings/things.rb",
31
+ "lib/harvestthings/things/projects.rb",
32
+ "lib/harvestthings/things/tasks.rb",
33
+ "pkg/harvestthings-0.1.0.gem",
34
+ "pkg/harvestthings-1.0.0.gem"
35
+ ]
36
+ s.homepage = %q{http://github.com/bjhess/HarvestThings}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{sync projects and tasks between Things and Harvest}
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<hpricot>, [">= 0.8.1"])
48
+ s.add_runtime_dependency(%q<net/http>, [">= 0"])
49
+ s.add_runtime_dependency(%q<jcode>, [">= 0"])
50
+ else
51
+ s.add_dependency(%q<hpricot>, [">= 0.8.1"])
52
+ s.add_dependency(%q<net/http>, [">= 0"])
53
+ s.add_dependency(%q<jcode>, [">= 0"])
54
+ end
55
+ else
56
+ s.add_dependency(%q<hpricot>, [">= 0.8.1"])
57
+ s.add_dependency(%q<net/http>, [">= 0"])
58
+ s.add_dependency(%q<jcode>, [">= 0"])
59
+ end
60
+ end
61
+
@@ -0,0 +1,50 @@
1
+ begin
2
+ require 'harvestthings/harvest'
3
+ require 'harvestthings/things'
4
+ require 'harvestthings/sync'
5
+ rescue LoadError => e
6
+ puts "there was an error loading a dependancy: #{e}"
7
+ end
8
+
9
+ module HarvestThings
10
+
11
+ class Application
12
+
13
+ # include sync mixin
14
+ include Sync
15
+
16
+ # initialize - defines a harvest and things object
17
+ #
18
+ # @return [Boolean]
19
+ def initialize
20
+ @harvest = Harvest.new
21
+ @things = Things.new
22
+ init_sync if config_checks?
23
+ end
24
+
25
+ # init_sync - kicks off the syncing
26
+ #
27
+ # @return [String]
28
+ def init_sync
29
+ print "starting sync"
30
+ things_projects_to_harvest
31
+ puts ".finished. ciao!"
32
+ end
33
+
34
+ private
35
+
36
+ # config_checks? - makes sure the config credentials are correct
37
+ #
38
+ # @return [Boolean]
39
+ def config_checks?
40
+ begin
41
+ response = @harvest.request '/clients', :get
42
+ rescue
43
+ exception = true
44
+ end
45
+ return exception == true ? false : true
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,148 @@
1
+ ########################################################################
2
+ #
3
+ # The full HARVEST API documentation can be found at:
4
+ #
5
+ # http://getharvest.com/api
6
+ #
7
+
8
+ # everything is in utf8
9
+ $KCODE = 'u'
10
+
11
+ require 'base64'
12
+ require 'bigdecimal'
13
+ require 'date'
14
+ require 'jcode'
15
+ require 'net/http'
16
+ require 'net/https'
17
+ require 'time'
18
+
19
+ class Harvest
20
+
21
+ # define Harvest config file path
22
+ CONFIG_PATH = File.join(Dir.pwd, "harvestthings", "harvest", "config.rb")
23
+
24
+ def initialize
25
+ generate_config unless File.exists?(CONFIG_PATH)
26
+ load CONFIG_PATH
27
+
28
+ @company = HarvestConfig.attrs[:subdomain]
29
+ @preferred_protocols = [HarvestConfig.attrs[:has_ssl], ! HarvestConfig.attrs[:has_ssl]]
30
+ connect!
31
+ end
32
+
33
+ # generate a config file if one doesn't exist
34
+ def generate_config
35
+ # define email
36
+ puts "enter the email you use to log into Harvest:"
37
+ email = gets
38
+ # define password
39
+ puts "enter the password for this Harvest account:"
40
+ password = gets
41
+ # define subdomain
42
+ puts "enter the subdomain for your Harvest account:"
43
+ subdomain = gets
44
+
45
+ str = <<EOS
46
+ class HarvestConfig
47
+ def self.attrs(overwrite = {})
48
+ {
49
+ :email => "#{email.chomp!}",
50
+ :password => "#{password.chomp!}",
51
+ :subdomain => "#{subdomain.chomp!}",
52
+ :has_ssl => false,
53
+ :user_agent => "Ruby/HarvestThings"
54
+ }.merge(overwrite)
55
+ end
56
+ end
57
+ EOS
58
+
59
+ File.open(CONFIG_PATH, 'w') {|f| f.write(str) }
60
+ end
61
+
62
+ # HTTP headers you need to send with every request.
63
+ def headers
64
+ {
65
+ # Declare that you expect response in XML after a _successful_
66
+ # response.
67
+ "Accept" => "application/xml",
68
+
69
+ # Promise to send XML.
70
+ "Content-Type" => "application/xml; charset=utf-8",
71
+
72
+ # All requests will be authenticated using HTTP Basic Auth, as
73
+ # described in rfc2617. Your library probably has support for
74
+ # basic_auth built in, I've passed the Authorization header
75
+ # explicitly here only to show what happens at HTTP level.
76
+ "Authorization" => "Basic #{auth_string}",
77
+
78
+ # Tell Harvest a bit about your application.
79
+ "User-Agent" => HarvestConfig.attrs[:user_agent]
80
+ }
81
+ end
82
+
83
+ def auth_string
84
+ Base64.encode64("#{HarvestConfig.attrs[:email]}:#{HarvestConfig.attrs[:password]}").delete("\r\n")
85
+ end
86
+
87
+ def request path, method = :get, body = ""
88
+ response = send_request( path, method, body)
89
+ if response.class < Net::HTTPSuccess
90
+ # response in the 2xx range
91
+ on_completed_request
92
+ return response
93
+ elsif response.class == Net::HTTPServiceUnavailable
94
+ # response status is 503, you have reached the API throttle
95
+ # limit. Harvest will send the "Retry-After" header to indicate
96
+ # the number of seconds your boot needs to be silent.
97
+ raise "Got HTTP 503 three times in a row" if retry_counter > 3
98
+ sleep(response['Retry-After'].to_i + 5)
99
+ request(path, method, body)
100
+ elsif response.class == Net::HTTPFound
101
+ # response was a redirect, most likely due to protocol
102
+ # mismatch. Retry again with a different protocol.
103
+ @preferred_protocols.shift
104
+ raise "Failed connection using http or https" if @preferred_protocols.empty?
105
+ connect!
106
+ request(path, method, body)
107
+ else
108
+ dump_headers = response.to_hash.map { |h,v| [h.upcase,v].join(': ') }.join("\n")
109
+ raise "#{response.message} (#{response.code})\n\n#{dump_headers}\n\n#{response.body}\n"
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def connect!
116
+ port = has_ssl ? 443 : 80
117
+ @connection = Net::HTTP.new("#{@company}.harvestapp.com", port)
118
+ @connection.use_ssl = has_ssl
119
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if has_ssl
120
+ end
121
+
122
+ def has_ssl
123
+ @preferred_protocols.first
124
+ end
125
+
126
+ def send_request path, method = :get, body = ''
127
+ case method
128
+ when :get
129
+ @connection.get(path, headers)
130
+ when :post
131
+ @connection.post(path, body, headers)
132
+ when :put
133
+ @connection.put(path, body, headers)
134
+ when :delete
135
+ @connection.delete(path, headers)
136
+ end
137
+ end
138
+
139
+ def on_completed_request
140
+ @retry_counter = 0
141
+ end
142
+
143
+ def retry_counter
144
+ @retry_counter ||= 0
145
+ @retry_counter += 1
146
+ end
147
+
148
+ end
@@ -0,0 +1,219 @@
1
+ module Sync
2
+
3
+ # things_projects_to_harvest - detemines which Things projects get sent to Harvest
4
+ #
5
+ # @return [Array] - array of projects
6
+ def things_projects_to_harvest
7
+ define_harvest_projects
8
+ define_harvest_tasks
9
+ define_harvest_clients
10
+
11
+ @things.projects.each do |project|
12
+ print "."
13
+ name = @things.project_title(project).downcase
14
+ client = @things.project_area(project).downcase
15
+ client_id = harvest_client?(client) ? harvest_client_id(client) : add_client_to_harvest(client)
16
+ add_project_to_harvest(name, client_id) unless harvest_project?(name)
17
+ things_tasks_to_harvest(project)
18
+ end
19
+ end
20
+
21
+ # things_tasks_to_harvest - determines which Things tasks get sent to Harvest
22
+ #
23
+ # @return [Array] - array of tasks
24
+ def things_tasks_to_harvest(project)
25
+ @things.tasks(project).each do |task|
26
+ unless @things.task_complete?(task) # complete in Things
27
+ task_desc = @things.task_description(task).downcase
28
+ add_task_to_harvest(@things.project_title(project).downcase, task_desc) unless harvest_task?(task_desc) # unless already exists in Harvest
29
+ end
30
+ end
31
+ end
32
+
33
+ # add_project_to_harvest - saves a Things project as a Harvest project
34
+ #
35
+ # @param [str] - the name of the Things project
36
+ # @param [str] - the Harvest client id
37
+ # @return [Boolean]
38
+ def add_project_to_harvest(proj_name, client)
39
+ puts " adding #{proj_name} to Harvest"
40
+ str = <<EOS
41
+ <project>
42
+ <name>#{proj_name}</name>
43
+ <active type="boolean">true</active>
44
+ <bill-by>none</bill-by>
45
+ <client-id type="integer">#{client}</client-id>
46
+ <code></code>
47
+ <notes></notes>
48
+ <budget type="decimal"></budget>
49
+ <budget-by>none</budget-by>
50
+ </project>
51
+ EOS
52
+ response = @harvest.request '/projects', :post, str
53
+ end
54
+
55
+ # add_task_to_harvest - saves a Things task as a Harvest task
56
+ #
57
+ # @param [str] - the Thing project
58
+ # @param [str] - the Things task description
59
+ # @return [String] - the cleaned string
60
+ def add_task_to_harvest(project_name, task_desc)
61
+ str = <<EOS
62
+ <task>
63
+ <billable-by-default type="boolean">true</billable-by-default>
64
+ <default-hourly-rate type="decimal"></default-hourly-rate>
65
+ <is-default type="boolean">false</is-default>
66
+ <name>#{task_desc}</name>
67
+ </task>
68
+ EOS
69
+ response = @harvest.request '/tasks', :post, str
70
+ new_task_location = response['Location']
71
+ new_task_id = new_task_location.gsub(/\/tasks\//, '')
72
+ assign_task_assignment(new_task_id, project_name)
73
+ end
74
+
75
+ # assign_task_assignment - assigns a newly created task to a Project in Harvest
76
+ #
77
+ # @param [Integer] - the new Harvest task ID
78
+ # @return [Integer] - the Harvest project ID
79
+ def assign_task_assignment(new_task_id, project_name)
80
+ str = <<EOS
81
+ <task>
82
+ <id type="integer">#{new_task_id}</id>
83
+ </task>
84
+ EOS
85
+ response = @harvest.request "/projects/#{harvest_project_id(project_name)}/task_assignments", :post, str
86
+ end
87
+
88
+ # add_client_to_harvest - saves a Things area_name as a Harvest client
89
+ #
90
+ # @param [str] - the Things area_name
91
+ # @return [Integer] - the new Harvest client id
92
+ def add_client_to_harvest(area_name)
93
+ str = <<EOS
94
+ <client>
95
+ <name>#{area_name}</name>
96
+ <details></details>
97
+ </client>
98
+ EOS
99
+ response = @harvest.request '/clients', :post, str
100
+ end
101
+
102
+
103
+
104
+ private
105
+
106
+ # define_harvest_tasks - loads all of the existing tasks from Harvest
107
+ #
108
+ # @return [Array] - array of tasks
109
+ def define_harvest_tasks
110
+ @harvest_tasks = []
111
+
112
+ response = @harvest.request '/tasks', :get
113
+ doc = Hpricot::XML(response.body)
114
+ (doc/:tasks/:task).each do |task|
115
+ temp = {}
116
+ ['name', 'id'].each do |el|
117
+ temp[el] = task.at(el).innerHTML.downcase
118
+ end
119
+ @harvest_tasks.push temp
120
+ end
121
+ end
122
+
123
+ # define_harvest_projects - loads all of the existing projects from Harvest
124
+ #
125
+ # @return [Array] - array of projects names
126
+ def define_harvest_projects
127
+ @harvest_projects = []
128
+
129
+ response = @harvest.request '/projects', :get
130
+ doc = Hpricot::XML(response.body)
131
+ (doc/:projects/:project).each do |project|
132
+ temp = {}
133
+ ['name', 'id'].each do |el|
134
+ temp[el] = project.at(el).innerHTML.downcase
135
+ end
136
+ @harvest_projects.push temp
137
+ end
138
+ end
139
+
140
+ # define_harvest_clients - loads all of the existing clients from Harvest
141
+ #
142
+ # @return [Array] - array of clients names
143
+ def define_harvest_clients
144
+ @harvest_clients = []
145
+
146
+ response = @harvest.request '/clients', :get
147
+ doc = Hpricot::XML(response.body)
148
+ (doc/:clients/:client).each do |client|
149
+ temp = {}
150
+ ['name', 'id'].each do |el|
151
+ temp[el] = client.at(el).innerHTML.downcase
152
+ end
153
+ @harvest_clients.push temp
154
+ end
155
+ end
156
+
157
+ # harvest_project? - checks to see if Things project already exists in Harvest
158
+ #
159
+ # @param [str] - the project name to check for
160
+ # @return [Boolean]
161
+ def harvest_project?(proj_name)
162
+ match = false
163
+ @harvest_projects.each do |project|
164
+ if project['name'] == proj_name
165
+ match = true
166
+ end
167
+ end
168
+ return match == false ? false : true
169
+ end
170
+
171
+ # harvest_client? - checks to see if Things area already exists as Harvest client
172
+ #
173
+ # @param [str] - the Things area name
174
+ # @return [integer] - the matching client id if it exists, otherwise false
175
+ def harvest_client?(area_name)
176
+ match = false
177
+ @harvest_clients.each do |client|
178
+ if client['name'] == area_name
179
+ match = true
180
+ end
181
+ end
182
+ return match == false ? false : true
183
+ end
184
+
185
+ # harvest_task? - checks to see if a Things task already exists in Harvest
186
+ #
187
+ # @param [str] - the task description
188
+ # @return [Boolean]
189
+ def harvest_task?(task_name)
190
+ match = false
191
+ @harvest_tasks.each do |task|
192
+ if task['name'] == task_name
193
+ match = true
194
+ end
195
+ end
196
+ return match == false ? false : true
197
+ end
198
+
199
+ # harvest_client_id - get the Harvest client id for a Things area name
200
+ #
201
+ # @param [str] - the Things area name
202
+ # @return [Integer] - the Harvest client id
203
+ def harvest_client_id(area_name)
204
+ @harvest_clients.each do |client|
205
+ return client['id'] if client['name'] == area_name
206
+ end
207
+ end
208
+
209
+ # harvest_project_id - get the Harvest project id for a Things project
210
+ #
211
+ # @param [str] - the Things project id number
212
+ # @return [Integer] - the Harvest project id
213
+ def harvest_project_id(proj_name)
214
+ @harvest_projects.each do |project|
215
+ return project['id'] if project['name'].downcase.to_s == proj_name.to_s
216
+ end
217
+ end
218
+
219
+ end
@@ -0,0 +1,66 @@
1
+ module Projects
2
+ # projects - grab an array of the various project ids from the xml
3
+ #
4
+ # @param [id] - the string of the project's id
5
+ # @return [Hpricot] - an Hpricot XML object
6
+ def projects
7
+ # find all projects from the first OBJECT node
8
+ first_obj = @xml.at('object')
9
+
10
+ if first_obj.search("relationship[@destination='TODO']").length != 0
11
+ first_obj.search("relationship[@destination='TODO']") do |elem| # older versions of Things
12
+ return elem.attributes["idrefs"].to_s.split(" ")
13
+ end
14
+ else
15
+ @xml.search("attribute[@name='title']") do |elem| # newer versions of Things
16
+ if elem.html == "Projects"
17
+ elem.parent.search("relationship[@name='focustodos']") do |e|
18
+ return e.attributes["idrefs"].to_s.split(" ")
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # project - grab the Hpricot element of the project using the id
27
+ #
28
+ # @param [id] - the string of the project's id
29
+ # @return [Hpricot] - an Hpricot XML object
30
+ def project(id)
31
+ @xml.search("object[@id='#{id}']")
32
+ end
33
+
34
+ # project_title - grab the title of the project using the id
35
+ #
36
+ # @param [id] - the string of the project's attribute id
37
+ # @return [String] - a cleaned and formatted title string
38
+ def project_title(id)
39
+ project = @xml.search("object[@id='#{id}']")
40
+ title = project.search("attribute[@name='title']")
41
+ clean(title.innerHTML.to_s)
42
+ end
43
+
44
+ # project_area - grab the area of the project using the id
45
+ #
46
+ # @param [id] - the string of the project's attribute id
47
+ # @return [String] - a cleaned and formatted area string
48
+ def project_area(id)
49
+ project = @xml.search("object[@id='#{id}']")
50
+ area = project.search("relationship[@name='parent']")
51
+ area_id = area.attr('idrefs').to_s
52
+ area_id == "" ? "default" : project_title(area_id)
53
+ end
54
+
55
+ private
56
+
57
+ # clean - clean a title string with specific rules
58
+ #
59
+ # @param [str] - the string to clean and return
60
+ # @return [String] - the cleaned string
61
+ def clean(str)
62
+ # remove any underscores
63
+ $temp = str.gsub("_", " ")
64
+ $temp = $temp.gsub(/^[a-z]|\s+[a-z]/) { |a| a.upcase }
65
+ end
66
+ end
@@ -0,0 +1,43 @@
1
+ module Tasks
2
+ # tasks - grab an array of the various task ids from the xml
3
+ #
4
+ # @param [id] - the string of the project's id
5
+ # @return [Array] - array of the various task ids
6
+ def tasks(id)
7
+ project = @xml.search("object[@id='#{id}']")
8
+ project.search("relationship[@name='children']") do |elem|
9
+ return elem.attributes["idrefs"].to_s.split(" ")
10
+ end
11
+ end
12
+
13
+ # task_description - grab formatted version of a task's description
14
+ #
15
+ # @param [id] - the tasks id
16
+ # @return [String] - a formatted string of the task description
17
+ def task_description(id)
18
+ task = @xml.search("object[@id='#{id}']")
19
+ title = task.search("attribute[@name='title']")
20
+ clean(title.innerHTML.to_s)
21
+ end
22
+
23
+ # task_complete? - boolean of whether the task is complete
24
+ #
25
+ # @param [id] - the task id
26
+ # @return [Boolean] - returns true of false
27
+ def task_complete?(id)
28
+ task = @xml.search("object[@id='#{id}']")
29
+ task.search("attribute[@name='datecompleted']").any?
30
+ end
31
+
32
+ private
33
+
34
+ # clean - clean a title string with specific rules
35
+ #
36
+ # @param [str] - the string to clean and return
37
+ # @return [String] - the cleaned string
38
+ def clean(str)
39
+ # remove any underscores
40
+ $temp = str.gsub("_", " ")
41
+ $temp = $temp.gsub(/^[a-z]|\s+[a-z]/) { |a| a.upcase }
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ require 'harvestthings/things/projects'
2
+ require 'harvestthings/things/tasks'
3
+
4
+ class Things
5
+ # include the projects mixin
6
+ include Projects
7
+
8
+ # include the tasks mixin
9
+ include Tasks
10
+
11
+ # Hpricot doc of Things xml file
12
+ attr_reader :xml
13
+
14
+ # Define default Things database file path and file name
15
+ DATABASE_PATH = "Library/Application\ Support/Cultured\ Code/Things"
16
+ DATABASE_FILE = "Database.xml"
17
+
18
+ # initialize - change to the default Things directory and load the xml
19
+ #
20
+ # @return [Boolean]
21
+ def initialize
22
+ current_pwd = Dir.pwd
23
+ Dir.chdir() # changes to HOME environment variable
24
+ Dir.chdir(DATABASE_PATH)
25
+ if File.exists?(DATABASE_FILE)
26
+ load_database
27
+ methods
28
+ else
29
+ raise SystemError, "can't find the default Things database file"
30
+ end
31
+ Dir.chdir(current_pwd)
32
+ end
33
+
34
+ # load_database - loads the databse file into the xml property
35
+ #
36
+ # @return [Hpricot]
37
+ def load_database
38
+ @xml = Hpricot.XML(open(DATABASE_FILE))
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #--
4
+
5
+ # Copyright 2009 by Michael Krisher (mike@mikekrisher.com)
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to
9
+ # deal in the Software without restriction, including without limitation the
10
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11
+ # sell copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23
+ # IN THE SOFTWARE.
24
+ #++
25
+
26
+ HARVESTTHINGSVERSION = '0.0.1'
27
+
28
+ # load gem dependancies
29
+ begin
30
+ require 'hpricot'
31
+ require 'net/http'
32
+ require 'uri'
33
+ rescue LoadError => e
34
+ puts "there was an error loading a gem: #{e}"
35
+ end
36
+
37
+ require 'harvestthings/application'
38
+
39
+ def harvestthings
40
+ HarvestThings::Application.new
41
+ return true
42
+ end
43
+
44
+
45
+
46
+
47
+
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: harvestthingstest
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Barry Hess
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-11 00:00:00 -06:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.8.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: net/http
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: jcode
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ description: harvestthings will sync your clients, projects, and tasks between Things and Harvest, where areas in Things correspond to clients in Harvest
46
+ email: barry@bjhess.com
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.mdown
53
+ - TODO
54
+ files:
55
+ - .gitignore
56
+ - README.mdown
57
+ - Rakefile
58
+ - TODO
59
+ - VERSION.yml
60
+ - harvestthingstest.gemspec
61
+ - lib/harvestthings.rb
62
+ - lib/harvestthings/application.rb
63
+ - lib/harvestthings/harvest.rb
64
+ - lib/harvestthings/sync.rb
65
+ - lib/harvestthings/things.rb
66
+ - lib/harvestthings/things/projects.rb
67
+ - lib/harvestthings/things/tasks.rb
68
+ - pkg/harvestthings-0.1.0.gem
69
+ - pkg/harvestthings-1.0.0.gem
70
+ has_rdoc: true
71
+ homepage: http://github.com/bjhess/HarvestThings
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options:
76
+ - --charset=UTF-8
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ version:
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.5
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: sync projects and tasks between Things and Harvest
98
+ test_files: []
99
+