toodledo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ module Toodledo
2
+
3
+ #
4
+ # A read only representation of a Goal.
5
+ #
6
+ class Goal
7
+
8
+ LIFE_LEVEL = 0
9
+
10
+ MEDIUM_LEVEL = 1
11
+
12
+ SHORT_LEVEL = 2
13
+
14
+ LEVEL_ARRAY = [ LIFE_LEVEL, MEDIUM_LEVEL, SHORT_LEVEL ]
15
+
16
+ def self.valid?(input)
17
+ for level in LEVEL_ARRAY
18
+ if (level == input)
19
+ return true
20
+ end
21
+ end
22
+ return false
23
+ end
24
+
25
+ def initialize(id, level, contributes_id, name)
26
+ @id = id
27
+ @level = level
28
+ @contributes_id = contributes_id
29
+ @name = name
30
+ end
31
+
32
+ NO_GOAL = Goal.new(0, 0, 0, "No goal")
33
+
34
+ attr_reader :level, :contributes_id, :name
35
+
36
+ def contributes
37
+ if (@contributes == nil)
38
+ return NO_GOAL
39
+ end
40
+ return @contributes
41
+ end
42
+
43
+ def contributes=(parent_goal)
44
+ @contributes = parent_goal
45
+ end
46
+
47
+ def server_id
48
+ return @id
49
+ end
50
+
51
+ # Parses a goal from an XML element.
52
+ def self.parse(session, el)
53
+ id = el.attributes['id']
54
+ level = el.attributes['level'].to_i
55
+ contributes_id = el.attributes['contributes']
56
+ name = el.text
57
+ goal = Goal.new(id, level, contributes_id, name)
58
+ return goal
59
+ end
60
+
61
+ def to_xml()
62
+ return "<goal id=\"#{@id}\" level=\"#{@level}\" contributes=\"#{@contributes.server_id}\" name=\"#{@name}\">"
63
+ end
64
+
65
+ def to_s()
66
+ msg = "$[#{name}]"
67
+ #if (contributes != NO_GOAL)
68
+ # msg += " (Contributes to: #{contributes.name})"
69
+ #end
70
+ return msg
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,8 @@
1
+ module Toodledo
2
+ #
3
+ # Thrown when a call to a server returns 'Invalid ID number'
4
+ #
5
+ class ItemNotFoundError < StandardError
6
+
7
+ end
8
+ end
@@ -0,0 +1,34 @@
1
+ module Toodledo
2
+
3
+ #
4
+ # A priority enum.
5
+ #
6
+ class Priority
7
+
8
+ NEGATIVE = -1
9
+
10
+ LOW = 0
11
+
12
+ MEDIUM = 1
13
+
14
+ HIGH = 2
15
+
16
+ TOP = 3
17
+
18
+ PRIORITY_ARRAY = [ TOP, HIGH, MEDIUM, LOW, NEGATIVE ]
19
+
20
+ def self.each
21
+ PRIORITY_ARRAY.each{|value| yield(value)}
22
+ end
23
+
24
+ def self.valid?(input)
25
+ for priority in PRIORITY_ARRAY
26
+ if input == priority
27
+ return true
28
+ end
29
+ end
30
+ return false
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # To change this template, choose Tools | Templates
3
+ # and open the template in the editor.
4
+
5
+
6
+ module Toodledo
7
+ class Repeat
8
+
9
+ NONE = 0
10
+ WEEKLY = 1
11
+ MONTHLY = 2
12
+ YEARLY = 3
13
+ DAILY = 4
14
+ BIWEEKLY = 5
15
+ BIMONTHLY = 6
16
+ SEMIANNUALLY = 7
17
+ QUARTERLY = 8
18
+
19
+ REPEAT_ARRAY = [
20
+ NONE,
21
+ WEEKLY,
22
+ MONTHLY,
23
+ YEARLY,
24
+ DAILY,
25
+ BIWEEKLY,
26
+ BIMONTHLY,
27
+ SEMIANNUALLY,
28
+ QUARTERLY
29
+ ]
30
+
31
+ def self.each
32
+ REPEAT_ARRAY.each{|value| yield(value)}
33
+ end
34
+
35
+ def self.valid?(input)
36
+ for repeat in REPEAT_ARRAY
37
+ if (repeat == input)
38
+ return true
39
+ end
40
+ end
41
+ return false
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module Toodledo
3
+
4
+ #
5
+ # Thrown when a call to the server fails.
6
+ #
7
+ class ServerError < StandardError
8
+
9
+ end
10
+ end
@@ -0,0 +1,1029 @@
1
+ require 'toodledo'
2
+
3
+ require 'digest/md5'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'net/https'
7
+ require 'openssl/ssl'
8
+ require 'rexml/document'
9
+ require 'logger'
10
+
11
+ module Toodledo
12
+
13
+ #
14
+ # The Session. This is responsible for calling to the server
15
+ # and handling most functionality.
16
+ #
17
+ class Session
18
+
19
+ DEFAULT_API_URL = 'http://www.toodledo.com/api.php'
20
+
21
+ USER_AGENT = "Ruby/#{Toodledo::VERSION} (#{RUBY_PLATFORM})"
22
+
23
+ HEADERS = {
24
+ 'User-Agent' => USER_AGENT
25
+ }
26
+
27
+ EXPIRATION_TIME_IN_SECS = 60 * 60
28
+
29
+ DATE_FORMAT = '%Y-%m-%d'
30
+
31
+ DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
32
+
33
+ TIME_FORMAT = '%I:%M %p'
34
+
35
+ attr_accessor :logger
36
+
37
+ attr_reader :base_url, :user_id, :proxy
38
+
39
+ # Creates a new session, using the given user name and password.
40
+ # throws exception if user_id or password are nil.
41
+ def initialize(user_id, password, logger = nil)
42
+ raise "Nil user_id" if (user_id == nil)
43
+ raise "Nil password" if (password == nil)
44
+
45
+ @user_id = user_id
46
+ @password = password
47
+
48
+ @folders = nil
49
+ @contexts = nil
50
+ @goals = nil
51
+ @key = nil
52
+ @folders_by_id = nil
53
+ @goals_by_id = nil
54
+ @contexts_by_id = nil
55
+
56
+ @logger = logger
57
+ end
58
+
59
+ # Hashes the input string and returns a string hex digest.
60
+ def md5(input_string)
61
+ return Digest::MD5.hexdigest(input_string)
62
+ end
63
+
64
+ # Connects to the server, asking for a new key that's good for an hour.
65
+ # Optionally takes a base URL as a parameter. Defaults to DEFAULT_API_URL.
66
+ def connect(base_url = DEFAULT_API_URL, proxy = nil)
67
+ logger.debug("connect(#{base_url}, #{proxy.inspect})") if logger
68
+
69
+ # XXX It looks like get_user_id doesn't work reliably. It always
70
+ # returns 1 even when we pass in a valid email and password.
71
+ # @user_id = get_user_id(@email, @password)
72
+ # logger.debug("user_id = #{@user_id}, #{@email} #{@password}")
73
+
74
+ if (@user_id == '1')
75
+ raise "No matching user_id found"
76
+ end
77
+
78
+ if (@user_id == '0')
79
+ raise "Server says we have a blank email or password"
80
+ end
81
+
82
+ # Set the base URL.
83
+ @base_url = base_url
84
+
85
+ # Get the proxy information if it exists.
86
+ @proxy = proxy
87
+
88
+ session_token = get_token(@user_id)
89
+ key = md5(md5(@password).to_s + session_token + @user_id);
90
+
91
+ @key = key
92
+ @start_time = Time.now
93
+ end
94
+
95
+ # Disconnects from the server.
96
+ def disconnect()
97
+ logger.debug("disconnect()") if logger
98
+ @key = nil
99
+ @start_time = nil
100
+ @base_url = nil
101
+ @proxy = nil
102
+ end
103
+
104
+ # Returns true if the session has expired.
105
+ def expired?
106
+ #logger.debug("expired?") too annoying
107
+
108
+ # The key is only good for an hour. If it's been over an hour,
109
+ # then we count it as expired.
110
+ return true if (@start_time == nil)
111
+ return (Time.now - @start_time > EXPIRATION_TIME_IN_SECS)
112
+ end
113
+
114
+ # Returns a parsable URI object from the base API URL and the parameters.
115
+ def make_uri(method, params)
116
+ url_string = URI.escape(@base_url + '?method=' + method + params)
117
+ return URI.parse(url_string)
118
+ end
119
+
120
+ # escape the & character as %26 and the ; character as %3B.
121
+ # throws an exception if input is nil.
122
+ def escape_text(input)
123
+ raise "Nil input" if (input == nil)
124
+ return input.to_s if (! input.kind_of? String)
125
+
126
+ output_string = input.gsub('&', '%26')
127
+ output_string = output_string.gsub(';', '%3B')
128
+ return output_string
129
+ end
130
+
131
+ # Calls Toodledo with the method name, the parameters and the session key.
132
+ # Returns the text inside the document root, if any.
133
+ def call(method, params, key = nil)
134
+ raise 'Nil method' if (method == nil)
135
+ raise 'Nil params' if (params == nil)
136
+ raise 'Wrong type of params' if (! params.kind_of? Hash)
137
+ raise 'Wrong method type' if (! method.kind_of? String)
138
+
139
+ if (@base_url == nil)
140
+ raise 'Must call connect() before this method'
141
+ end
142
+
143
+ # Break all the parameters down into key=value seperated by semi colons
144
+ stringified_params = (key != nil) ? ';key=' + key : ''
145
+
146
+ params.each { |k, v|
147
+ stringified_params += ';' + k.to_s + '=' + escape_text(v)
148
+ }
149
+ url = make_uri(method, stringified_params)
150
+
151
+ # If it's been more than an hour, then ask for a new key.
152
+ if (@key != nil && expired?)
153
+ logger.debug("call(#{method}) connection expired, reconnecting...") if logger
154
+
155
+ # Save the connection information (we'll need it)
156
+ base_url = @base_url
157
+ proxy = @proxy
158
+ disconnect() # ensures that key == nil, which is crucial to avoid an endless loop...
159
+ connect(base_url, proxy)
160
+ end
161
+
162
+ # Establish the proxy
163
+ if (@proxy != nil)
164
+ logger.debug("call(#{method}) establishing proxy...") if logger
165
+
166
+ proxy_host = @proxy['host']
167
+ proxy_port = @proxy['port']
168
+ proxy_user = @proxy['user']
169
+ proxy_password = @proxy['password']
170
+
171
+ if (proxy_user == nil || proxy_password == nil)
172
+ http = Net::HTTP::Proxy(proxy_host, proxy_port).new(url.host, url.port)
173
+ else
174
+ http = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_password).new(url.host, url.port)
175
+ end
176
+ else
177
+ http = Net::HTTP.new(url.host, url.port)
178
+ end
179
+
180
+ if (url.scheme == 'https')
181
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
182
+ http.use_ssl = true
183
+ end
184
+
185
+ if logger
186
+ logger.debug("call(#{method}) request: #{url.path}?#{url.query}#{url.fragment}")
187
+ end
188
+ start_time = Time.now
189
+
190
+ # make the call
191
+ response = http.request_get(url.request_uri, HEADERS)
192
+ body = response.body
193
+
194
+ # body = url.read
195
+ end_time = Time.now
196
+ doc = REXML::Document.new body
197
+
198
+ if logger
199
+ logger.debug("call(#{method}) response: " + doc.to_s)
200
+ logger.debug("call(#{method}) time: " + (end_time - start_time).to_s + ' seconds')
201
+ end
202
+
203
+ root_node = doc.root
204
+ if (root_node.name == 'error')
205
+ error_text = root_node.text
206
+ if (error_text == 'Invalid ID number')
207
+ raise Toodledo::ItemNotFoundError.new(error_text)
208
+ else
209
+ raise Toodledo::ServerError.new(error_text)
210
+ end
211
+ end
212
+
213
+ return root_node
214
+ end
215
+
216
+ # Gets the token method, given the id.
217
+ def get_token(user_id)
218
+ raise "Nil user_id" if (user_id == nil)
219
+
220
+ params = { :userid => user_id }
221
+ result = call('getToken', params)
222
+ return result.text
223
+ end
224
+
225
+ # Returns the user id. As far as I can tell, this method is broken
226
+ # and always returns false.
227
+ #
228
+ # If the userid comes back as 0, it means that either the email
229
+ # or password that you sent was blank. If the userid comes back as 1,
230
+ # it means that the lookup failed. A valid userid will always be a
231
+ # 15 or 16 character hexadecimal string.
232
+ def get_user_id(email, password)
233
+ raise "Nil email" if (email == nil)
234
+ raise "Nil password" if (password == nil)
235
+
236
+ params = { :email => email, :pass => password }
237
+ result = call('getUserid', params)
238
+ return result.text
239
+ end
240
+
241
+ ############################################################################
242
+ # Tasks
243
+ ############################################################################
244
+
245
+ #
246
+ # Gets tasks that meet the criteria given in params. Available criteria is
247
+ # as follows:
248
+
249
+ # * title:
250
+ # * folder:
251
+ # * context:
252
+ # * goal:
253
+ # * duedate:
254
+ # * duetime
255
+ # * repeat:
256
+ # * priority:
257
+ # * parent:
258
+ # * shorter:
259
+ # * longer:
260
+ # * before
261
+ # * after
262
+ # * modbefore
263
+ # * modafter
264
+ # * compbefore
265
+ # * compafter
266
+ # * notcomp
267
+ #
268
+ # Returns an array of tasks. This information is never cached.
269
+ def get_tasks(params={})
270
+ logger.debug("get_tasks(#{params.inspect})") if logger
271
+ myhash = {}
272
+
273
+ # * title : A text string up to 255 characters.
274
+ handle_string(myhash, params, :title)
275
+
276
+ # If the folder is a string, then we assume we're supposed to find out what
277
+ # the id is.
278
+ handle_folder(myhash, params)
279
+
280
+ # Context handling
281
+ handle_context(myhash, params)
282
+
283
+ # Goal handling
284
+ handle_goal(myhash, params)
285
+
286
+ # duedate handling. Take either a string or a Time object.'YYYY-MM-DD'
287
+ # This does not need special handling, because if it's not a time object
288
+ # then we don't pass through anything at all.
289
+ handle_date(myhash, params, :duedate)
290
+
291
+ # duetime handling. Take either a string or a Time object.
292
+ handle_time(myhash, params, :duetime)
293
+
294
+ # repeat: takes in an integer in the proper range..
295
+ handle_repeat(myhash, params)
296
+
297
+ # priority: takes in an integer in the proper range.
298
+ handle_priority(myhash, params)
299
+
300
+ # * parent : This is used to Pro accounts that have access to subtasks.
301
+ # Set this to the id number of the parent task and you will get its
302
+ # subtasks. The default is 0, which is a special number that returns
303
+ # tasks that do not have a parent.
304
+ handle_parent(myhash, params)
305
+
306
+ # * shorter : An integer representing minutes. This is used for finding
307
+ # tasks with a duration that is shorter than the specified number of minutes.
308
+ handle_number(myhash, params, :shorter)
309
+
310
+ # * longer : An integer representing minutes. This is used for finding
311
+ # tasks with a duration that is longer than the specified number of minutes.
312
+ handle_number(myhash, params, :longer)
313
+
314
+ # * before : A date formated as YYYY-MM-DD. Used to find tasks with
315
+ # due-dates before this date.
316
+ handle_date(myhash, params, :before)
317
+
318
+ # * after : A date formated as YYYY-MM-DD. Used to find tasks with
319
+ # due-dates after this date.
320
+ handle_date(myhash, params, :after)
321
+
322
+ # * modbefore : A date-time formated as YYYY-MM-DD HH:MM:SS. Used to find
323
+ # tasks with a modified date and time before this dateand time.
324
+ handle_datetime(myhash, params, :modbefore)
325
+
326
+ # * modafter : A date-time formated as YYYY-MM-DD HH:MM:SS. Used to find
327
+ # tasks with a modified date and time after this dateand time.
328
+ handle_datetime(myhash, params, :modafter)
329
+
330
+ # * compbefore : A date formated as YYYY-MM-DD. Used to find tasks with a
331
+ # completed date before this date.
332
+ handle_date(myhash, params, :compbefore)
333
+
334
+ # * compafter : A date formated as YYYY-MM-DD. Used to find tasks with a
335
+ # completed date after this date.
336
+ handle_date(myhash, params, :compafter)
337
+
338
+ # * notcomp : Set to 1 to omit completed tasks. Omit variable, or set to 0
339
+ # to retrieve both completed and uncompleted tasks.
340
+ handle_boolean(myhash, params, :notcomp)
341
+
342
+ result = call('getTasks', myhash, @key)
343
+ tasks = []
344
+ result.elements.each do |el|
345
+ task = Task.parse(self, el)
346
+ tasks << task
347
+ end
348
+ return tasks
349
+ end
350
+
351
+ # Adds a task to Toodledo.
352
+ #
353
+ # Required Parameters:
354
+ # title: a String. This is the only required property.
355
+ #
356
+ # Optional Parameters:
357
+ # tag: a String
358
+ # folder: folder id or String matching the folder name
359
+ # context: context id or String matching the context name
360
+ # goal: goal id or String matching the Goal Name
361
+ # duedate: Time or String object "YYYY-MM-DD". If this is a string, it
362
+ # may take an optional modifier.
363
+ # duetime: Time or String object "MM:SS p"}
364
+ # parent: parent id }
365
+ # repeat: one of { :none, :weekly, :monthly :yearly :daily :biweekly,
366
+ # :bimonthly, :semiannually, :quarterly }
367
+ # length: a Number, number of minutes
368
+ # priority: one of { :negative, :low, :medium, :high, :top }
369
+ #
370
+ # Returns: the id of the added task as a String.
371
+ def add_task(title, params={})
372
+ logger.debug("add_task(#{title}, #{params.inspect})") if logger
373
+ raise "Nil id" if (title == nil)
374
+
375
+ myhash = {:title => title}
376
+
377
+ handle_string(myhash, params, :tag)
378
+
379
+ # If the folder is a string, then we assume we're supposed to find out what
380
+ # the id is.
381
+ handle_folder(myhash, params)
382
+
383
+ # Context handling
384
+ handle_context(myhash, params)
385
+
386
+ # Goal handling
387
+ handle_goal(myhash, params)
388
+
389
+ # duedate handling. Take either a string or a Time object.'YYYY-MM-DD'
390
+ handle_date(myhash, params, :duedate)
391
+
392
+ # duetime handling. Take either a string or a Time object.
393
+ handle_time(myhash, params, :duetime)
394
+
395
+ # parent handling.
396
+ handle_parent(myhash, params)
397
+
398
+ # repeat: use the map to change from the symbol to the raw numeric value.
399
+ handle_repeat(myhash, params)
400
+
401
+ # priority use the map to change from the symbol to the raw numeric value.
402
+ handle_priority(myhash, params)
403
+
404
+ handle_string(myhash, params, :note)
405
+
406
+ result = call('addTask', myhash, @key)
407
+
408
+ return result.text
409
+ end
410
+
411
+ # * id : The id number of the task to edit.
412
+ # * title : A text string up to 255 characters representing the name of the task.
413
+ # * folder : The id number of the folder.
414
+ # * context : The id number of the context.
415
+ # * goal : The id number of the goal.
416
+ # * completed : true or false.
417
+ # * duedate : A date formatted as YYYY-MM-DD with an optional modifier
418
+ # attached to the front. Examples: "2007-01-23" , "=2007-01-23" ,
419
+ # ">2007-01-23". To unset the date, set it to '0000-00-00'.
420
+ # * duetime : A date formated as HH:MM MM.
421
+ # * repeat : Use the REPEAT_MAP with the relevant symbol here.
422
+ # * length : An integer representing the number of minutes that the task will take to complete.
423
+ # * priority : Use the PRIORITY_MAP with the relevant symbol here.
424
+ # * note : A text string.
425
+ def edit_task(id, params = {})
426
+ logger.debug("edit_task(#{id}, #{params.inspect})") if logger
427
+ raise "Nil id" if (id == nil)
428
+
429
+ myhash = { :id => id }
430
+
431
+ handle_string(myhash, params, :tag)
432
+
433
+ # If the folder is a string, then we assume we're supposed to find out what
434
+ # the id is.
435
+ handle_folder(myhash, params)
436
+
437
+ # Context handling
438
+ handle_context(myhash, params)
439
+
440
+ # Goal handling
441
+ handle_goal(myhash, params)
442
+
443
+ # duedate handling. Take either a string or a Time object.'YYYY-MM-DD'
444
+ handle_date(myhash, params, :duedate)
445
+
446
+ # duetime handling. Take either a string or a Time object.
447
+ handle_time(myhash, params, :duetime)
448
+
449
+ # parent handling.
450
+ handle_parent(myhash, params)
451
+
452
+ # Handle completion.
453
+ handle_boolean(myhash, params, :completed)
454
+
455
+ # repeat: use the map to change from the symbol to the raw numeric value.
456
+ handle_repeat(myhash, params)
457
+
458
+ # priority use the map to change from the symbol to the raw numeric value.
459
+ handle_priority(myhash, params)
460
+
461
+ handle_string(myhash, params, :note)
462
+
463
+ result = call('editTask', myhash, @key)
464
+
465
+ return (result.text == '1')
466
+ end
467
+
468
+ #
469
+ # Deletes the task, using the task id.
470
+ #
471
+ def delete_task(id)
472
+ logger.debug("delete_task(#{id})") if logger
473
+ raise "Nil id" if (id == nil)
474
+
475
+ result = call('deleteTask', { :id => id }, @key)
476
+
477
+ return (result.text == '1')
478
+ end
479
+
480
+ ############################################################################
481
+ # Contexts
482
+ ############################################################################
483
+
484
+ #
485
+ # Returns the context with the given name.
486
+ #
487
+ def get_context_by_name(context_name)
488
+ logger.debug("get_context_by_name(#{context_name})") if logger
489
+
490
+ if (@contexts_by_name == nil)
491
+ get_contexts(true)
492
+ end
493
+
494
+ context = @contexts_by_name[context_name.downcase]
495
+ return context
496
+ end
497
+
498
+ #
499
+ # Returns the context with the given id.
500
+ #
501
+ def get_context_by_id(context_id)
502
+ logger.debug("get_context_by_id(#{context_id})") if logger
503
+
504
+ if (@contexts_by_id == nil)
505
+ get_contexts(true)
506
+ end
507
+
508
+ context = @contexts_by_id[context_id]
509
+ return context
510
+ end
511
+
512
+ #
513
+ # Gets the array of contexts.
514
+ #
515
+ def get_contexts(flush = false)
516
+ logger.debug("get_contexts(#{flush})") if logger
517
+ return @contexts unless (flush || @contexts == nil)
518
+
519
+ result = call('getContexts', {}, @key)
520
+ contexts_by_name = {}
521
+ contexts_by_id = {}
522
+ contexts = []
523
+
524
+ result.elements.each { |el|
525
+ context = Context.parse(self, el)
526
+ contexts << context
527
+ contexts_by_id[context.server_id] = context
528
+ contexts_by_name[context.name.downcase] = context
529
+ }
530
+ @contexts_by_id = contexts_by_id
531
+ @contexts_by_name = contexts_by_name
532
+ @contexts = contexts
533
+ return contexts
534
+ end
535
+
536
+ #
537
+ # Adds the context to Toodledo, with the title.
538
+ #
539
+ def add_context(title)
540
+ logger.debug("add_context(#{title})") if logger
541
+ raise "Nil title" if (title == nil)
542
+
543
+ result = call('addContext', { :title => title }, @key)
544
+
545
+ flush_contexts()
546
+
547
+ return result.text
548
+ end
549
+
550
+ #
551
+ # Deletes the context from Toodledo, using the id.
552
+ #
553
+ def delete_context(id)
554
+ logger.debug("delete_context(#{id})") if logger
555
+ raise "Nil id" if (id == nil)
556
+
557
+ result = call('deleteContext', { :id => id }, @key)
558
+
559
+ flush_contexts();
560
+
561
+ return (result.text == '1')
562
+ end
563
+
564
+ #
565
+ # Deletes the cached contexts.
566
+ #
567
+ def flush_contexts()
568
+ logger.debug('flush_contexts()') if logger
569
+
570
+ @contexts_by_id = nil
571
+ @contexts_by_name = nil
572
+ @contexts = nil
573
+ end
574
+
575
+ ############################################################################
576
+ # Goals
577
+ ############################################################################
578
+
579
+ #
580
+ # Returns the goal with the given name. Case insensitive.
581
+ #
582
+ def get_goal_by_name(goal_name)
583
+ logger.debug("get_goal_by_name(#{goal_name})") if logger
584
+ if (@goals_by_name == nil)
585
+ get_goals(true)
586
+ end
587
+
588
+ goal = @goals_by_name[goal_name.downcase]
589
+ return goal
590
+ end
591
+
592
+ #
593
+ # Returns the goal with the given id.
594
+ #
595
+ def get_goal_by_id(goal_id)
596
+ logger.debug("get_goal_by_id(#{goal_id})") if logger
597
+ if (@goals_by_id == nil)
598
+ get_goals(true)
599
+ end
600
+
601
+ goal = @goals_by_id[goal_id]
602
+ return goal
603
+ end
604
+
605
+ #
606
+ # Returns the array of goals.
607
+ #
608
+ def get_goals(flush = false)
609
+ logger.debug("get_goals(#{flush})") if logger
610
+ return @goals unless (flush || @goals == nil)
611
+
612
+ result = call('getGoals', {}, @key)
613
+
614
+ goals_by_name = {}
615
+ goals_by_id = {}
616
+ goals = []
617
+ result.elements.each do |el|
618
+ goal = Goal.parse(self, el)
619
+ goals << goal
620
+ goals_by_id[goal.server_id] = goal
621
+ goals_by_name[goal.name.downcase] = goal
622
+ end
623
+
624
+ # Loop through and make sure we've got a reference for every contributing
625
+ # goal.
626
+ for goal in goals
627
+ next if (goal.contributes_id == Goal::NO_GOAL.server_id)
628
+ parent_goal = goals_by_id[goal.contributes_id]
629
+ goal.contributes = parent_goal
630
+ end
631
+
632
+ @goals = goals
633
+ @goals_by_name = goals_by_name
634
+ @goals_by_id = goals_by_id
635
+ return goals
636
+ end
637
+
638
+ #
639
+ # Adds a new goal with the given title, the level (short to long term) and
640
+ # the contributing goal id.
641
+ #
642
+ def add_goal(title, level = 0, contributes = 0)
643
+ logger.debug("add_goal(#{title}, #{level}, #{contributes})") if logger
644
+ raise "Nil title" if (title == nil)
645
+
646
+ params = { :title => title, :level => level, :contributes => contributes }
647
+ result = call('addGoal', params, @key)
648
+
649
+ flush_goals()
650
+
651
+ return result.text
652
+ end
653
+
654
+ #
655
+ # Delete the goal with the given id.
656
+ #
657
+ def delete_goal(id)
658
+ logger.debug("delete_goal(#{id})") if logger
659
+ raise "Nil id" if (id == nil)
660
+
661
+ result = call('deleteGoal', { :id => id }, @key)
662
+
663
+ flush_goals()
664
+
665
+ return (result.text == '1')
666
+ end
667
+
668
+ #
669
+ # Nils the cached goals.
670
+ #
671
+ def flush_goals()
672
+ logger.debug('flush_goals()') if logger
673
+
674
+ @goals = nil
675
+ @goals_by_name = nil
676
+ @goals_by_id = nil
677
+ end
678
+
679
+ ############################################################################
680
+ # Folders
681
+ ############################################################################
682
+
683
+ #
684
+ # Gets the folder by the name. Case insensitive.
685
+ def get_folder_by_name(folder_name)
686
+ logger.debug("get_folders_by_name(#{folder_name})") if logger
687
+ raise "Nil folder name" if (folder_name == nil)
688
+
689
+ if (@folders_by_name == nil)
690
+ get_folders(true)
691
+ end
692
+
693
+ return @folders_by_name[folder_name.downcase]
694
+ end
695
+
696
+ #
697
+ # Gets the folder with the given id.
698
+ #
699
+ def get_folder_by_id(folder_id)
700
+ logger.debug("get_folder_by_id(#{folder_id})") if logger
701
+ raise "Nil folder_id" if (folder_id == nil)
702
+
703
+ if (@folders_by_id == nil)
704
+ get_folders(true)
705
+ end
706
+
707
+ return @folders_by_id[folder_id]
708
+ end
709
+
710
+ # Gets all the folders.
711
+ def get_folders(flush = false)
712
+ logger.debug("get_folders(#{flush})") if logger
713
+ return @folders unless (flush || @folders == nil)
714
+
715
+ result = call('getFolders', {}, @key)
716
+ # <folders>
717
+ # <folder id="123" private="0" archived="0">Shopping</folder>
718
+ # <folder id="456" private="0" archived="0">Home Repairs</folder>
719
+ # <folder id="789" private="0" archived="0">Vacation Planning</folder>
720
+ # <folder id="234" private="0" archived="0">Chores</folder>
721
+ # <folder id="567" private="1" archived="0">Work</folder>
722
+ # </folders>
723
+ folders = []
724
+ folders_by_name = {}
725
+ folders_by_id = {}
726
+ result.elements.each { |el|
727
+ folder = Folder.parse(self, el)
728
+ folders.push(folder)
729
+ folders_by_name[folder.name.downcase] = folder # lowercase the key search
730
+ folders_by_id[folder.server_id] = folder
731
+ }
732
+ @folders = folders
733
+ @folders_by_name = folders_by_name
734
+ @folders_by_id = folders_by_id
735
+ return @folders
736
+ end
737
+
738
+ # Adds a folder.
739
+ # * title : A text string up to 32 characters.
740
+ # * private : A boolean value that describes if this folder can be shared.
741
+ #
742
+ # Returns the id of the newly added folder.
743
+ def add_folder(title, is_private = 1)
744
+ logger.debug("add_folder(#{title}, #{is_private})") if logger
745
+ raise "Nil title" if (title == nil)
746
+
747
+ if (is_private.kind_of? TrueClass)
748
+ is_private = 1
749
+ elsif (is_private.kind_of? FalseClass)
750
+ is_private = 0
751
+ end
752
+
753
+ myhash = { :title => title, :private => is_private}
754
+
755
+ result = call('addFolder', myhash, @key)
756
+
757
+ flush_folders()
758
+
759
+ return result.text
760
+ end
761
+
762
+ #
763
+ # Nils out the cached folders.
764
+ #
765
+ def flush_folders()
766
+ logger.debug("flush_folders()") if logger
767
+
768
+ @folders = nil
769
+ @folders_by_name = nil
770
+ @folders_by_id = nil
771
+ end
772
+
773
+ # Edits a folder.
774
+ # * id : The id number of the folder to edit.
775
+ # * title : A text string up to 32 characters.
776
+ # * private : A boolean value (0 or 1) that describes if this folder can be
777
+ # shared. A value of 1 means that this folder is private.
778
+ # * archived : A boolean value (0 or 1) that describes if this folder is archived.
779
+ #
780
+ # Returns true if the edit was successful.
781
+ def edit_folder(id, params = {})
782
+ logger.debug("edit_folder(#{id}, #{params.inspect})") if logger
783
+ raise "Nil id" if (id == nil)
784
+
785
+ myhash = { :id => id }
786
+
787
+ handle_string(myhash, params, :title)
788
+
789
+ handle_boolean(myhash, params, :private)
790
+
791
+ handle_boolean(myhash, params, :archived)
792
+
793
+ result = call('editFolder', myhash, @key)
794
+
795
+ flush_folders()
796
+
797
+ return (result.text == '1')
798
+ end
799
+
800
+ # Deletes the folder with the id.
801
+ # id : The folder id.
802
+ #
803
+ # Returns true if the delete was successful.
804
+ def delete_folder(id)
805
+ logger.debug("delete_folder(#{id})") if logger
806
+ raise "Nil id" if (id == nil)
807
+
808
+ result = call('deleteFolder', { :id => id }, @key)
809
+
810
+ flush_folders()
811
+
812
+ return (result.text == '1')
813
+ end
814
+
815
+ ############################################################################
816
+ # Helper methods follow.
817
+ #
818
+ # These methods will convert the appropriate format for talking to the
819
+ # Toodledo server. They do not parse the XML that comes back from the
820
+ # server.
821
+ ############################################################################
822
+
823
+ def handle_number(myhash, params, symbol)
824
+ value = params[symbol]
825
+ if (value != nil)
826
+ if (value.kind_of? FixNum)
827
+ myhash.merge!({ symbol => value.to_s})
828
+ end
829
+ end
830
+ end
831
+
832
+ def handle_boolean(myhash, params, symbol)
833
+ value = params[symbol]
834
+ if (value == nil)
835
+ return
836
+ end
837
+
838
+ case value
839
+ when TrueClass, FalseClass
840
+ bool = (value == true) ? '1' : '0'
841
+ when String
842
+ bool = ('true' == value.downcase) ? '1' : '0'
843
+ when FixNum
844
+ bool = (value == 1) ? '1' : '0'
845
+ else
846
+ bool = value
847
+ end
848
+
849
+ myhash.merge!({ symbol => bool })
850
+ end
851
+
852
+ def handle_string(myhash, params, symbol)
853
+ value = params[symbol]
854
+ if (value != nil)
855
+ myhash.merge!({ symbol => value })
856
+ end
857
+ end
858
+
859
+ def handle_date(myhash, params, symbol)
860
+ value = params[symbol]
861
+ if (value == nil)
862
+ return
863
+ end
864
+
865
+ case value
866
+ when Time
867
+ value = value.strftime('%Y-%m-%d')
868
+ end
869
+
870
+ myhash.merge!({ symbol => value })
871
+ end
872
+
873
+ def handle_time(myhash, params, symbol)
874
+ value = params[symbol]
875
+ if (value == nil)
876
+ return
877
+ end
878
+
879
+ case value
880
+ when Time
881
+ value = value.strftime(TIME_FORMAT)
882
+ end
883
+
884
+ myhash.merge!({ symbol => value })
885
+ end
886
+
887
+ # Handles a generic date time value.
888
+ def handle_datetime(myhash, params, symbol)
889
+ # YYYY-MM-DD HH:MM:SS
890
+ value = params[symbol]
891
+ if (value == nil)
892
+ return
893
+ end
894
+
895
+ case value
896
+ when Time
897
+ value = value.strftime(DATETIME_FORMAT)
898
+ end
899
+
900
+ myhash.merge!({ symbol => value })
901
+ end
902
+
903
+ # Handles the parent task object. Only takes a task object or id.
904
+ def handle_parent(myhash, params)
905
+ parent = params[:parent]
906
+ if (parent == nil)
907
+ return
908
+ end
909
+
910
+ parent_id = nil
911
+ case parent
912
+ when Task
913
+ parent_id = parent.server_id
914
+ else
915
+ parent_id = parent
916
+ end
917
+
918
+ myhash.merge!({ :parent => parent_id })
919
+ end
920
+
921
+ # Handles a folder (in the form of a folder name, object or id) and puts
922
+ # the folder_id in myhash.
923
+ def handle_folder(myhash, params)
924
+ folder = params[:folder]
925
+ if (folder == nil)
926
+ return
927
+ end
928
+
929
+ folder_id = nil
930
+ case folder
931
+ when String
932
+ folder_obj = get_folder_by_name(folder)
933
+ if (folder_obj == nil)
934
+ raise Toodledo::ItemNotFoundError.new("No folder found with name #{folder}")
935
+ end
936
+ folder_id = folder_obj.server_id
937
+ when Folder
938
+ folder_id = folder.server_id
939
+ else
940
+ folder_id = folder
941
+ end
942
+
943
+ myhash.merge!({ :folder => folder_id })
944
+ end
945
+
946
+ # Takes in a context (in the form of a string, a Context object or
947
+ # a context id) and adds it to myhash as a context_id.
948
+ def handle_context(myhash, params)
949
+ context = params[:context]
950
+ if (context == nil)
951
+ return
952
+ end
953
+
954
+ case context
955
+ when String
956
+ context_obj = get_context_by_name(context)
957
+ if (context_obj == nil)
958
+ raise Toodledo::ItemNotFoundError.new("No context found with name '#{context}'")
959
+ end
960
+ context_id = context_obj.server_id
961
+ when Context
962
+ context_id = context.server_id
963
+ else
964
+ context_id = context
965
+ end
966
+
967
+ myhash.merge!({ :context => context_id })
968
+ end
969
+
970
+ # Takes a goal (as a goal title, a goal object or a goal id) and sets it
971
+ # in myhash.
972
+ def handle_goal(myhash, params)
973
+ goal = params[:goal]
974
+ if (goal == nil)
975
+ return
976
+ end
977
+
978
+ case goal
979
+ when String
980
+ goal_obj = get_goal_by_name(goal)
981
+ if (goal_obj == nil)
982
+ raise Toodledo::ItemNotFoundError.new("No goal found with name '#{goal}'")
983
+ end
984
+ goal_id = goal_obj.server_id
985
+ when Goal
986
+ goal_id = goal.server_id
987
+ else
988
+ goal_id = goal
989
+ end
990
+
991
+ # Otherwise, assume it's a number.
992
+ myhash.merge!({ :goal => goal_id })
993
+ end
994
+
995
+ # Handles the repeat parameter.
996
+ def handle_repeat(myhash, params)
997
+ repeat = params[:repeat]
998
+
999
+ if (repeat == nil)
1000
+ return
1001
+ end
1002
+
1003
+ if (! Repeat.valid?(repeat))
1004
+ raise "Invalid repeat value: #{repeat}"
1005
+ end
1006
+
1007
+ myhash.merge!({ :repeat => repeat })
1008
+ end
1009
+
1010
+ # Handles the priority. This must be one of several values.
1011
+ def handle_priority(myhash, params)
1012
+ priority = params[:priority]
1013
+
1014
+ if (priority == nil)
1015
+ return nil
1016
+ end
1017
+
1018
+ if (! Priority.valid?(priority))
1019
+ raise "Invalid priority value: #{priority}"
1020
+ end
1021
+
1022
+ myhash.merge!({ :priority => priority })
1023
+ end
1024
+
1025
+ end
1026
+ end
1027
+
1028
+
1029
+