toodledo 1.3.5 → 1.3.8

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.
@@ -9,6 +9,7 @@ module Toodledo
9
9
  #
10
10
  module ParserHelper
11
11
 
12
+ # TODO These regexps are highly repetitive. Refactor
12
13
  FOLDER_REGEXP = /\*((\w+)|\[(.*?)\])/
13
14
 
14
15
  GOAL_REGEXP = /\^((\w+)|\[(.*?)\])/
@@ -17,6 +18,10 @@ module Toodledo
17
18
 
18
19
  PRIORITY_REGEXP = /!(top|high|medium|low|negative)/
19
20
 
21
+ DATE_REGEXP = /\#(([^\[]\S*)|\[(.*?)\])/
22
+
23
+ TAGS_REGEXP = /\%((\w+)|\[(.*?)\])/
24
+
20
25
  # Note that level must exist at the beginning of the line
21
26
  LEVEL_REGEXP = /^(life|medium|short)/
22
27
 
@@ -25,7 +30,9 @@ module Toodledo
25
30
  FOLDER_REGEXP,
26
31
  GOAL_REGEXP,
27
32
  CONTEXT_REGEXP,
28
- PRIORITY_REGEXP
33
+ PRIORITY_REGEXP,
34
+ DATE_REGEXP,
35
+ TAGS_REGEXP
29
36
  ]
30
37
 
31
38
  # Parses a context in the format @Context or @[Spaced Context]
@@ -48,9 +55,25 @@ module Toodledo
48
55
  return match_data if (match_data == nil)
49
56
  return strip_brackets(match_data[1])
50
57
  end
58
+
59
+ # Parses a date in the format #[2011-03-17]
60
+ def parse_date(input)
61
+ match_data = DATE_REGEXP.match(input)
62
+ return match_data if (match_data == nil)
63
+ return strip_brackets(match_data[1])
64
+ end
65
+
66
+ # Parses a list of tags in the format %[tag1 tag2 tag3]
67
+ # TODO Allow %tag1, tag2, tag3 ? (This is the format Toodledo's Twitter client uses)
68
+ def parse_tag(input)
69
+ match_data = TAGS_REGEXP.match(input)
70
+ return match_data if (match_data == nil)
71
+ return strip_brackets(match_data[1]).split(/\s+/)
72
+ end
51
73
 
52
74
  # Parses priority in the format !priority (top, high, medium, low,
53
75
  # negative)
76
+ # TODO Refactor using data-driven design
54
77
  def parse_priority(input)
55
78
  match_data = PRIORITY_REGEXP.match(input)
56
79
  if (match_data == nil)
@@ -1,151 +1,175 @@
1
-
2
- module Toodledo
3
- module CommandLine
4
- class TaskFormatter
5
-
6
- # Formats the task for a command line.
7
- def format(task)
8
- fancyp = '!' + readable_priority(task.priority)
9
-
10
- msg = "<#{task.server_id}> -- #{fancyp}"
11
-
12
- if (task.folder != Folder::NO_FOLDER)
13
- msg += " *[#{task.folder.name}]"
14
- end
15
-
16
- if (task.context != Context::NO_CONTEXT)
17
- msg += " @[#{task.context.name}]"
18
- end
19
-
20
- if (task.goal != Goal::NO_GOAL)
21
- msg += " ^[#{task.goal.name}]"
22
- end
23
-
24
- if (task.repeat != Repeat::NONE)
25
- msg += " repeat[#{readable_repeat(task.repeat)}]"
26
- end
27
-
28
- if (task.duedate != nil)
29
- fmt = '%m/%d/%Y %I:%M %p'
30
- msg += " \#[#{task.duedatemodifier}#{task.duedate.strftime(fmt)}]"
31
- end
32
-
33
- if (task.startdate != nil)
34
- fmt = '%m/%d/%Y'
35
- msg += " startdate[#{task.startdate.strftime(fmt)}]"
36
- end
37
-
38
- if (task.status != Status::NONE)
39
- msg += " status[#{readable_status(task.status)}]"
40
- end
41
-
42
- if (task.star)
43
- msg += " starred"
44
- end
45
-
46
- if (task.tag != nil)
47
- msg += " tag[#{task.tag}]"
48
- end
49
-
50
- if (task.parent_id != nil)
51
- msg += " parent[#{task.parent.title}]"
52
- end
53
-
54
- if (task.length != nil)
55
- msg += " length[#{task.length}]"
56
- end
57
-
58
- if (task.timer != nil)
59
- msg += " timer[#{task.timer}]"
60
- end
61
-
62
- if (task.num_children != nil && task.num_children > 0)
63
- msg += " children[#{task.num_children}]"
64
- end
65
-
66
- msg += " #{task.title}"
67
-
68
- if (task.note != nil)
69
- msg += "\n #{task.note}"
70
- end
71
-
72
- return msg
73
- end
74
-
75
- def readable_priority(priority)
76
- case priority
77
- when Priority::TOP
78
- return 'top'
79
- when Priority::HIGH
80
- return 'high'
81
- when Priority::MEDIUM
82
- return 'medium'
83
- when Priority::LOW
84
- return 'low'
85
- when Priority::NEGATIVE
86
- return 'negative'
87
- else
88
- return ''
89
- end
90
- end
91
-
92
- #
93
- # Returns a string matching the numeric repeat code.
94
- #
95
- def readable_repeat(repeat)
96
- case repeat
97
- when Repeat::NONE
98
- ''
99
- when Repeat::WEEKLY
100
- "weekly"
101
- when Repeat::MONTHLY
102
- "monthly"
103
- when Repeat::YEARLY
104
- "yearly"
105
- when Repeat::DAILY
106
- "daily"
107
- when Repeat::BIWEEKLY
108
- "biweekly"
109
- when Repeat::BIMONTHLY
110
- "bimonthly"
111
- when Repeat::SEMIANNUALLY
112
- "semiannually"
113
- when Repeat::QUARTERLY
114
- "quarterly"
115
- else
116
- ''
117
- end
118
- end
119
-
120
- #
121
- # Return a readable status given the numeric code.
122
- #
123
- def readable_status(status)
124
- case status
125
- when Status::NONE
126
- 'none'
127
- when Status::NEXT_ACTION
128
- 'Next Action'
129
- when Status::ACTIVE
130
- 'Active'
131
- when Status::PLANNING
132
- 'Planning'
133
- when Status::DELEGATED
134
- 'Delegated'
135
- when Status::WAITING
136
- 'Waiting'
137
- when Status::HOLD
138
- 'Hold'
139
- when Status::POSTPONED
140
- 'Postponed'
141
- when Status::SOMEDAY
142
- 'Someday'
143
- when Status::CANCELLED
144
- 'Cancelled'
145
- when Status::REFERENCE
146
- 'Reference'
147
- end
148
- end
149
- end
150
- end
1
+
2
+ module Toodledo
3
+ module CommandLine
4
+ class TaskFormatter
5
+
6
+ DUEDATE_BY = 0
7
+ DUEDATE_ON = 1
8
+ DUEDATE_AFTER = 2
9
+ DUEDATE_OPTIONALLY = 3
10
+
11
+ # Formats the task for a command line.
12
+ def format(task)
13
+ fancyp = '!' + readable_priority(task.priority)
14
+
15
+ msg = "<#{task.server_id}> -- #{fancyp}"
16
+
17
+ # TODO Only include [ ] if needed
18
+ if (task.folder != Folder::NO_FOLDER)
19
+ msg += " *[#{task.folder.name}]"
20
+ end
21
+
22
+ if (task.context != Context::NO_CONTEXT)
23
+ msg += " @[#{task.context.name}]"
24
+ end
25
+
26
+ if (task.goal != Goal::NO_GOAL)
27
+ msg += " ^[#{task.goal.name}]"
28
+ end
29
+
30
+ if (task.repeat != Repeat::NONE)
31
+ msg += " repeat[#{readable_repeat(task.repeat)}]"
32
+ end
33
+
34
+ if (task.duedate != nil)
35
+ fmt = '%m/%d/%Y %I:%M %p'
36
+ msg += " #[#{readable_duedatemodifier(task.duedatemodifier)}#{task.duedate.strftime(fmt)}]"
37
+ end
38
+
39
+ if (task.startdate != nil)
40
+ fmt = '%m/%d/%Y'
41
+ msg += " startdate[#{task.startdate.strftime(fmt)}]"
42
+ end
43
+
44
+ if (task.status != Status::NONE)
45
+ msg += " status[#{readable_status(task.status)}]"
46
+ end
47
+
48
+ if (task.star)
49
+ msg += " starred"
50
+ end
51
+
52
+ if (task.tag != nil)
53
+ msg += " %[#{task.tag}]"
54
+ end
55
+
56
+ if (task.parent_id != nil)
57
+ msg += " parent[#{task.parent.title}]"
58
+ end
59
+
60
+ if (task.length != nil)
61
+ msg += " length[#{task.length}]"
62
+ end
63
+
64
+ if (task.timer != nil)
65
+ msg += " timer[#{task.timer}]"
66
+ end
67
+
68
+ if (task.num_children != nil && task.num_children > 0)
69
+ msg += " children[#{task.num_children}]"
70
+ end
71
+
72
+ msg += " #{task.title}"
73
+
74
+ if (task.note != nil)
75
+ msg += "\n #{task.note}"
76
+ end
77
+
78
+ return msg
79
+ end
80
+
81
+ # TODO Refactor using symbols -- so much simpler to convert
82
+ def readable_priority(priority)
83
+ case priority
84
+ when Priority::TOP
85
+ return 'top'
86
+ when Priority::HIGH
87
+ return 'high'
88
+ when Priority::MEDIUM
89
+ return 'medium'
90
+ when Priority::LOW
91
+ return 'low'
92
+ when Priority::NEGATIVE
93
+ return 'negative'
94
+ else
95
+ return ''
96
+ end
97
+ end
98
+
99
+ def readable_duedatemodifier(duedate_modifier)
100
+ # The modifier is passed in as [0..3] but may come back as
101
+ # <duedate modifier='?'>2011-06-30</duedate>
102
+ case duedate_modifier
103
+ when DUEDATE_BY then
104
+ return ''
105
+ when DUEDATE_ON then
106
+ return '='
107
+ when DUEDATE_AFTER then
108
+ return '>'
109
+ when DUEDATE_OPTIONALLY then
110
+ return '?'
111
+ else
112
+ return duedate_modifier
113
+ end
114
+ end
115
+
116
+ #
117
+ # Returns a string matching the numeric repeat code.
118
+ #
119
+ def readable_repeat(repeat)
120
+ case repeat
121
+ when Repeat::NONE
122
+ ''
123
+ when Repeat::WEEKLY
124
+ "weekly"
125
+ when Repeat::MONTHLY
126
+ "monthly"
127
+ when Repeat::YEARLY
128
+ "yearly"
129
+ when Repeat::DAILY
130
+ "daily"
131
+ when Repeat::BIWEEKLY
132
+ "biweekly"
133
+ when Repeat::BIMONTHLY
134
+ "bimonthly"
135
+ when Repeat::SEMIANNUALLY
136
+ "semiannually"
137
+ when Repeat::QUARTERLY
138
+ "quarterly"
139
+ else
140
+ ''
141
+ end
142
+ end
143
+
144
+ #
145
+ # Return a readable status given the numeric code.
146
+ #
147
+ def readable_status(status)
148
+ case status
149
+ when Status::NONE
150
+ 'none'
151
+ when Status::NEXT_ACTION
152
+ 'Next Action'
153
+ when Status::ACTIVE
154
+ 'Active'
155
+ when Status::PLANNING
156
+ 'Planning'
157
+ when Status::DELEGATED
158
+ 'Delegated'
159
+ when Status::WAITING
160
+ 'Waiting'
161
+ when Status::HOLD
162
+ 'Hold'
163
+ when Status::POSTPONED
164
+ 'Postponed'
165
+ when Status::SOMEDAY
166
+ 'Someday'
167
+ when Status::CANCELLED
168
+ 'Cancelled'
169
+ when Status::REFERENCE
170
+ 'Reference'
171
+ end
172
+ end
173
+ end
174
+ end
151
175
  end
@@ -3,6 +3,7 @@ module Toodledo
3
3
  #
4
4
  # A priority enum.
5
5
  #
6
+ # TODO Refactor using symbols?
6
7
  class Priority
7
8
 
8
9
  NEGATIVE = -1
@@ -4,6 +4,7 @@
4
4
  module Toodledo
5
5
  class Repeat
6
6
 
7
+ # TODO Refactor
7
8
  NONE = 0
8
9
  WEEKLY = 1
9
10
  MONTHLY = 2
@@ -32,6 +33,7 @@ module Toodledo
32
33
  REPEAT_ARRAY.each{|value| yield(value)}
33
34
  end
34
35
 
36
+ # TODO replace with include?
35
37
  def self.valid?(input)
36
38
  for repeat in REPEAT_ARRAY
37
39
  if (repeat == input)
@@ -8,10 +8,27 @@ module Toodledo
8
8
 
9
9
  end
10
10
 
11
+ #
12
+ # Thrown when a call to a server returns 'Invalid ID number'
13
+ #
14
+ class ItemNotFoundError < ServerError
15
+
16
+ end
17
+
11
18
  # Thrown when the key is invalid (usually because we're using an old key or
12
19
  # the expiration timed out for some reason)
13
20
  class InvalidKeyError < ServerError
14
21
 
15
22
  end
16
23
 
24
+ # Thrown when no key is specified at all.
25
+ class NoKeySpecifiedError < ServerError
26
+
27
+ end
28
+
29
+ # Thrown when too many requests have been made.
30
+ class ExcessiveTokenRequestsError < ServerError
31
+
32
+ end
33
+
17
34
  end
@@ -17,6 +17,11 @@ module Toodledo
17
17
  #
18
18
  class Session
19
19
 
20
+ INVALID_ID_MESSAGE = 'Invalid ID number'
21
+ INVALID_KEY_MESSAGE = 'key did not validate'
22
+ NO_KEY_SPECIFIED_MESSAGE = 'No Key Specified'
23
+ EXCESSIVE_TOKEN_MESSAGE = 'Excessive API token requests over the last 1 hour. This user is temporarily blocked.'
24
+
20
25
  DEFAULT_API_URL = 'http://www.toodledo.com/api.php'
21
26
 
22
27
  USER_AGENT = "Ruby/#{Toodledo::VERSION} (#{RUBY_PLATFORM})"
@@ -27,8 +32,8 @@ module Toodledo
27
32
  'Keep-Alive' => '300'
28
33
  }
29
34
 
30
- # Make it a little less than 4 hours, so the file should always be fresher.
31
- FILE_EXPIRATION_TIME_IN_SECS = (60 * 60 * 4) - 300
35
+ # Make file expiration be 4 hours.
36
+ FILE_EXPIRATION_TIME_IN_SECS = (60 * 60 * 4)
32
37
 
33
38
  DATE_FORMAT = '%Y-%m-%d'
34
39
 
@@ -67,7 +72,7 @@ module Toodledo
67
72
  return Digest::MD5.hexdigest(input_string)
68
73
  end
69
74
 
70
- # Connects to the server, asking for a new key that's good for an hour.
75
+ # Connects to the server, asking for a new key that's good for four hours.
71
76
  # Optionally takes a base URL as a parameter. Defaults to DEFAULT_API_URL.
72
77
  def connect(base_url = DEFAULT_API_URL, proxy = nil)
73
78
  logger.debug("connect(#{base_url}, #{proxy.inspect})") if logger
@@ -105,10 +110,12 @@ module Toodledo
105
110
  def disconnect()
106
111
  logger.debug("disconnect()") if logger
107
112
 
108
- token_path = get_token_file(user_id)
109
- if token_path
110
- logger.debug("deleting token path()") if logger
111
- File.delete(token_path)
113
+ if expired?
114
+ token_path = get_token_file(user_id)
115
+ if token_path
116
+ logger.debug("deleting token path(#{token_path})") if logger
117
+ File.delete(token_path)
118
+ end
112
119
  end
113
120
 
114
121
  @key = nil
@@ -126,10 +133,11 @@ module Toodledo
126
133
 
127
134
  # Returns true if the session has expired.
128
135
  def expired?
136
+ logger.debug("Expiration time #{@expiration_time} ") if logger
129
137
  current_time = Time.now
130
138
  has_expired = (@expiration_time != nil) && (current_time > @expiration_time)
131
139
  if (has_expired)
132
- logger.debug("#{@expiration_time} > #{current_time}, expired == true") if logger
140
+ logger.debug("Expiration time #{@expiration_time} > current time #{current_time}, expired == true") if logger
133
141
  end
134
142
  has_expired
135
143
  end
@@ -240,21 +248,22 @@ module Toodledo
240
248
  if (root_node.name == 'error')
241
249
  error_text = root_node.text
242
250
  case error_text
243
- when 'Invalid ID number' then
251
+ when INVALID_ID_MESSAGE then
244
252
  raise Toodledo::ItemNotFoundError.new(error_text)
245
- when 'key did not validate', 'No Key Specified' then
246
- retry_error = true
253
+ when INVALID_KEY_MESSAGE then
254
+ disconnect()
255
+ raise Toodledo::InvalidKeyError.new(error_text)
256
+ when NO_KEY_SPECIFIED_MESSAGE then
257
+ disconnect()
258
+ raise Toodledo::NoKeySpecifiedError.new(error_text)
259
+ when EXCESSIVE_TOKEN_MESSAGE
260
+ disconnect()
261
+ raise Toodledo::ExcessiveTokenRequestsError.new(error_text)
247
262
  else
248
263
  raise Toodledo::ServerError.new(error_text)
249
264
  end
250
265
  end
251
266
 
252
- if (retry_error)
253
- logger.debug("call(#{method}): invalid key, reconnecting") if logger
254
- reconnect(@base_url, @proxy);
255
- root_node = call(method, params);
256
- end
257
-
258
267
  return root_node
259
268
  end
260
269
 
@@ -462,11 +471,14 @@ module Toodledo
462
471
  # * star
463
472
  # * status
464
473
  # * startdate
474
+ # * tag
465
475
  #
466
476
  # Returns an array of tasks. This information is never cached.
467
477
  def get_tasks(params={})
468
478
  logger.debug("get_tasks(#{params.inspect})") if logger
469
479
  myhash = {}
480
+
481
+ # TODO Very repetitious. Refactor
470
482
 
471
483
  # * title : A text string up to 255 characters.
472
484
  handle_string(myhash, params, :title)
@@ -548,6 +560,8 @@ module Toodledo
548
560
  # * notcomp : Set to 1 to omit completed tasks. Omit variable, or set to 0
549
561
  # to retrieve both completed and uncompleted tasks.
550
562
  handle_boolean(myhash, params, :notcomp)
563
+
564
+ handle_tag(myhash, params)
551
565
 
552
566
  result = call('getTasks', myhash, @key)
553
567
  tasks = []
@@ -587,6 +601,7 @@ module Toodledo
587
601
  # :bimonthly, :semiannually, :quarterly }
588
602
  # length: a Number, number of minutes
589
603
  # priority: one of { :negative, :low, :medium, :high, :top }
604
+ # tag: an Array of strings
590
605
  #
591
606
  # Returns: the id of the added task as a String.
592
607
  def add_task(title, params={})
@@ -625,6 +640,8 @@ module Toodledo
625
640
  # priority use the map to change from the symbol to the raw numeric value.
626
641
  handle_priority(myhash, params)
627
642
 
643
+ handle_tag(myhash, params)
644
+
628
645
  # Handle the star.
629
646
  handle_boolean(myhash, params, :star)
630
647
 
@@ -653,6 +670,7 @@ module Toodledo
653
670
  # * length : An integer representing the number of minutes that the task will take to complete.
654
671
  # * priority : Use the PRIORITY_MAP with the relevant symbol here.
655
672
  # * note : A text string.
673
+ # * tag: An Array of Strings (tags)
656
674
  def edit_task(id, params = {})
657
675
  logger.debug("edit_task(#{id}, #{params.inspect})") if logger
658
676
  raise "Nil id" if (id == nil)
@@ -691,6 +709,8 @@ module Toodledo
691
709
 
692
710
  # priority use the map to change from the symbol to the raw numeric value.
693
711
  handle_priority(myhash, params)
712
+
713
+ handle_tag(myhash, params)
694
714
 
695
715
  handle_string(myhash, params, :note)
696
716
 
@@ -1075,7 +1095,7 @@ module Toodledo
1075
1095
  def handle_number(myhash, params, symbol)
1076
1096
  value = params[symbol]
1077
1097
  if (value != nil)
1078
- if (value.kind_of? FixNum)
1098
+ if (value.kind_of? Integer)
1079
1099
  myhash.merge!({ symbol => value.to_s})
1080
1100
  end
1081
1101
  end
@@ -1122,6 +1142,21 @@ module Toodledo
1122
1142
  myhash.merge!({ symbol => value })
1123
1143
  end
1124
1144
 
1145
+
1146
+ def handle_tag(myhash, params)
1147
+ value = params[:tag]
1148
+ if (value == nil)
1149
+ return
1150
+ end
1151
+
1152
+ case value
1153
+ when Array
1154
+ value = value.join(' ')
1155
+ end
1156
+
1157
+ myhash.merge!({ :tag => value })
1158
+ end
1159
+
1125
1160
  def handle_time(myhash, params, symbol)
1126
1161
  value = params[symbol]
1127
1162
  if (value == nil)