toodledo 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.
@@ -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
+