toodledo 1.3.5 → 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)