harvestthings 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ lib/harvestthings/harvest/config.rb
data/README.mdown ADDED
@@ -0,0 +1,81 @@
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
+
41
+ The QUnit overlay looks like this in the browser:
42
+
43
+ [![](http://img.skitch.com/20091125-jptpbxfbcg4irp81ytnwf3fkxf.jpg)](http://img.skitch.com/20091125-jptpbxfbcg4irp81ytnwf3fkxf.jpg)
44
+
45
+
46
+ Requirements
47
+ =======
48
+
49
+ The HarvestThings gem requires a few other gems and libraries in order to make
50
+ the API calls:
51
+
52
+ * hpricot
53
+ * net/http
54
+ * net/http
55
+ * uri
56
+ * base64
57
+ * bigdecimal
58
+ * date
59
+ * time
60
+ * jcode
61
+
62
+ Usage
63
+ =====
64
+ as a gem
65
+
66
+
67
+ rquire rubygems
68
+ require harvestthings
69
+ harvestthings
70
+
71
+
72
+ TODO
73
+ ====
74
+ * add tests
75
+
76
+
77
+ Copyright (c) 2009 Michael Krisher, released under the MIT license
78
+
79
+ [http://culturedcode.com/things/]: http://culturedcode.com/things/
80
+ [http://www.getharvest.com/]: http://www.getharvest.com/
81
+ [http://github.com/mkrisher/harvestthings/]: http://github.com/mkrisher/harvestthings/
data/Rakefile ADDED
@@ -0,0 +1,19 @@
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 = "harvestthings"
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 = "mike@mikekrisher.com"
12
+ gemspec.homepage = "http://github.com/mkrisher/HarvestThings"
13
+ gemspec.authors = ["Michael Krisher"]
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ 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 ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,51 @@
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{harvestthings}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Michael Krisher"]
12
+ s.date = %q{2009-12-03}
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{mike@mikekrisher.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",
25
+ "harvestthings.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
+ ]
35
+ s.homepage = %q{http://github.com/mkrisher/HarvestThings}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.5}
39
+ s.summary = %q{sync projects and tasks between Things and Harvest}
40
+
41
+ if s.respond_to? :specification_version then
42
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
43
+ s.specification_version = 3
44
+
45
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
46
+ else
47
+ end
48
+ else
49
+ end
50
+ end
51
+
@@ -0,0 +1,49 @@
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 'rubygems'
31
+ gem "hpricot", ">= 0.8.1"
32
+ require 'hpricot'
33
+ require 'net/http'
34
+ require 'uri'
35
+ rescue LoadError => e
36
+ puts "there was an error loading a gem: #{e}"
37
+ end
38
+
39
+ require 'harvestthings/application'
40
+
41
+ def harvestthings
42
+ HarvestThings::Application.new
43
+ return true
44
+ end
45
+
46
+
47
+
48
+
49
+
@@ -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,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,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
Binary file
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: harvestthings
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Krisher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-03 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: harvestthings will sync your clients, projects, and tasks between Things and Harvest, where areas in Things correspond to clients in Harvest
17
+ email: mike@mikekrisher.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.mdown
24
+ - TODO
25
+ files:
26
+ - .gitignore
27
+ - README.mdown
28
+ - Rakefile
29
+ - TODO
30
+ - VERSION
31
+ - harvestthings.gemspec
32
+ - lib/harvestthings.rb
33
+ - lib/harvestthings/application.rb
34
+ - lib/harvestthings/harvest.rb
35
+ - lib/harvestthings/sync.rb
36
+ - lib/harvestthings/things.rb
37
+ - lib/harvestthings/things/projects.rb
38
+ - lib/harvestthings/things/tasks.rb
39
+ - pkg/harvestthings-0.1.0.gem
40
+ has_rdoc: true
41
+ homepage: http://github.com/mkrisher/HarvestThings
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.5
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: sync projects and tasks between Things and Harvest
68
+ test_files: []
69
+