gwtf 0.4.4 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,6 +13,7 @@ command [:edit, :vi, :e] do |c|
13
13
  begin
14
14
  tmp = Tempfile.new("gwtf")
15
15
  tmp.puts "Subject: %s" % [ item.subject ]
16
+ tmp.puts "Due Date: %s" % [ item.due_date ]
16
17
  tmp.puts "Description:"
17
18
  tmp.puts item.description if item.description
18
19
  tmp.rewind
@@ -23,13 +24,28 @@ command [:edit, :vi, :e] do |c|
23
24
  tmp.unlink
24
25
  end
25
26
 
26
- if edited_item.split("\n").first =~ /^Subject:\s*(.+)/
27
+ edited_item = edited_item.split("\n")
28
+
29
+ if edited_item.first =~ /^Subject:\s*(.+)/
27
30
  item.subject = $1
28
31
  else
29
32
  raise "Subject is required"
30
33
  end
31
34
 
32
- description = edited_item.split("\n")[2..-1]
35
+ if edited_item[1] =~ /^Due Date:\s*(.+)/
36
+ if ["", " "].include?($1)
37
+ item.due_date = nil
38
+ else
39
+ item.due_date = $1
40
+ end
41
+ else
42
+ item.due_date = nil # if the user just delete the line from the
43
+ edited_item.insert(1, "") # treat it as removing the due date but insert
44
+ # some blank data in the array to not break the
45
+ # description finding logic
46
+ end
47
+
48
+ description = edited_item[4..-1]
33
49
 
34
50
  unless [description].flatten.compact.empty?
35
51
  item.description = description.join("\n")
@@ -23,10 +23,20 @@ command [:list, :ls, :l] do |c|
23
23
  items = Gwtf::Items.new(File.join(global_options[:data], project), project)
24
24
  stats = items.stats
25
25
 
26
- puts "%#{longest_name + 3}s: open: %3d: closed %3d: total: %3d" % [ project, stats["open"], stats["closed"], stats["total"] ] unless stats["open"] == 0
26
+ unless stats["open"] == 0
27
+ msg = "%#{longest_name + 3}s: open: %-3d closed %-3d overdue: %-3d total: %-3d" % [ project, stats["open"], stats["closed"], stats["overdue"], stats["total"] ]
28
+
29
+ if stats["overdue"] > 0
30
+ puts Gwtf.red(msg)
31
+ elsif stats["due_soon"] > 0
32
+ puts Gwtf.yellow(msg)
33
+ else
34
+ puts Gwtf.green(msg)
35
+ end
36
+ end
27
37
  end
28
-
29
38
  puts
39
+
30
40
  elsif options[:overview]
31
41
  Gwtf.projects(global_options[:data]).each do |project|
32
42
  items = Gwtf::Items.new(File.join(global_options[:data], project), project)
@@ -1,6 +1,10 @@
1
1
  desc 'Create an item'
2
2
  arg_name 'Short item description'
3
3
  command [:new, :add, :n, :a, :c] do |c|
4
+ c.desc 'Due date for the item (yyyy/mm/dd)'
5
+ c.default_value false
6
+ c.flag [:due]
7
+
4
8
  c.desc 'Invoke EDITOR to provide a long form description'
5
9
  c.default_value false
6
10
  c.switch [:edit, :e]
@@ -43,6 +47,7 @@ command [:new, :add, :n, :a, :c] do |c|
43
47
  item = @items.new_item
44
48
  item.subject = subject
45
49
  item.description = description if description
50
+ item.due_date = options[:due] if options[:due]
46
51
  item.save
47
52
 
48
53
  if options[:remind]
data/lib/gwtf/item.rb CHANGED
@@ -1,10 +1,19 @@
1
1
  module Gwtf
2
2
  class Item
3
+ include ObjHash
4
+
3
5
  attr_accessor :file
4
6
  attr_reader :project
5
7
 
8
+ property :description, :default => nil, :validation => String
9
+ property :subject, :default => nil, :validation => String
10
+ property :status, :default => "open", :validation => ["open", "closed"]
11
+ property :item_id, :default => nil, :validation => Integer
12
+ property :work_log, :default => [], :validation => Array
13
+ property :due_date, :default => nil, :validation => /^20\d\d[- \/.](0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])$/
14
+ property :closed_at, :default => nil
15
+
6
16
  def initialize(file=nil, project=nil)
7
- @item = default_item
8
17
  @file = file
9
18
  @project = project
10
19
 
@@ -12,19 +21,33 @@ module Gwtf
12
21
  end
13
22
 
14
23
  def open?
15
- @item["status"] == "open"
24
+ status == "open"
16
25
  end
17
26
 
18
27
  def closed?
19
28
  !open?
20
29
  end
21
30
 
31
+ def overdue?
32
+ if has_due_date? && open?
33
+ return !!(days_till_due < 0)
34
+ else
35
+ return false
36
+ end
37
+ end
38
+
39
+ def days_till_due
40
+ return 1000 unless has_due_date?
41
+
42
+ return (Date.parse(due_date) - Date.today).to_i
43
+ end
44
+
22
45
  def load_item
23
46
  raise "A file to read from has not been specified" unless @file
24
47
 
25
48
  read_item = JSON.parse(File.read(@file))
26
49
 
27
- @item.merge!(read_item)
50
+ merge!(read_item)
28
51
  end
29
52
 
30
53
  def backup_dir
@@ -32,7 +55,7 @@ module Gwtf
32
55
  end
33
56
 
34
57
  def save(backup=true)
35
- raise "No item_id set, cannot save item" unless @item["item_id"]
58
+ raise "No item_id set, cannot save item" unless item_id
36
59
 
37
60
  if backup && File.exist?(@file)
38
61
  backup_name = File.basename(@file) + "-" + Time.now.to_f.to_s
@@ -47,17 +70,6 @@ module Gwtf
47
70
  @file
48
71
  end
49
72
 
50
- def default_item
51
- {"description" => nil,
52
- "subject" => nil,
53
- "created_at" => Time.now,
54
- "edited_at" => nil,
55
- "closed_at" => nil,
56
- "status" => "open",
57
- "item_id" => nil,
58
- "work_log" => []}
59
- end
60
-
61
73
  def time_worked
62
74
  work_log.inject(0) do |result, log|
63
75
  begin
@@ -74,12 +86,14 @@ module Gwtf
74
86
  flag << "closed" if closed?
75
87
  flag << "open" if open?
76
88
  flag << "descr" if description?
89
+ flag << "overdue" if overdue?
77
90
  flag << work_log.size.to_s unless work_log.empty?
78
91
  flag
79
92
  end
80
93
 
81
94
  def compact_flags
82
95
  flags = []
96
+ flags << "O" if overdue?
83
97
  flags << "D" if has_description?
84
98
  flags << "C" if closed?
85
99
  flags << "L" unless work_log.empty?
@@ -87,14 +101,30 @@ module Gwtf
87
101
  flags
88
102
  end
89
103
 
104
+ def colorize_by_due_date(string)
105
+ if overdue?
106
+ return Gwtf.red(string)
107
+ elsif days_till_due <= 1 && open?
108
+ return Gwtf.yellow(string)
109
+ else
110
+ return string
111
+ end
112
+ end
113
+
90
114
  def summary
91
115
  summary = StringIO.new
92
116
 
93
117
  summary.puts " Subject: %s" % [ subject ]
94
118
  summary.puts " Status: %s" % [ status ]
119
+
120
+ if has_due_date? && open?
121
+ due = "%s (%d days)" % [ due_date, days_till_due ]
122
+ summary.puts " Due Date: %s" % [ colorize_by_due_date(due) ]
123
+ end
124
+
95
125
  summary.puts "Time Worked: %s" % [ Gwtf.seconds_to_human(time_worked) ]
96
- summary.puts " Created: %s" % [ Time.parse(created_at).strftime("%F %R") ]
97
- summary.puts " Closed: %s" % [ Time.parse(closed_at).strftime("%F %R") ] if closed?
126
+ summary.puts " Created: %s" % [ Time.parse(created_at.to_s).strftime("%F %R") ]
127
+ summary.puts " Closed: %s" % [ Time.parse(closed_at.to_s).strftime("%F %R") ] if closed?
98
128
  summary.puts " ID: %s" % [ item_id ]
99
129
 
100
130
  if has_description?
@@ -124,17 +154,13 @@ module Gwtf
124
154
  end
125
155
 
126
156
  def to_s
127
- "%5s %-4s%-12s%8s" % [ item_id, compact_flags.join, Time.parse(created_at.to_s).strftime("%F"), subject ]
128
- end
129
-
130
- def to_hash
131
- @item
157
+ colorize_by_due_date("%5s %-4s%-10s %s" % [ item_id, compact_flags.join, has_due_date? ? due_date : "", subject ])
132
158
  end
133
159
 
134
160
  def record_work(text, elapsed=0)
135
161
  update_property(:edited_at, Time.now)
136
162
 
137
- @item["work_log"] << {"text" => text, "time" => Time.now, "elapsed" => elapsed}
163
+ work_log << {"text" => text, "time" => Time.now, "elapsed" => elapsed}
138
164
  end
139
165
 
140
166
  def open
@@ -147,35 +173,6 @@ module Gwtf
147
173
  update_property(:status, "closed")
148
174
  end
149
175
 
150
- def to_json
151
- JSON.pretty_generate(@item)
152
- end
153
-
154
- def to_yaml
155
- @item.to_yaml
156
- end
157
-
158
- def update_property(property, value)
159
- property = property.to_s
160
-
161
- raise "No such property: #{property}" unless @item.include?(property)
162
-
163
- @item["edited_at"] = Time.now
164
- @item[property] = value
165
- end
166
-
167
- def [](property)
168
- property = property.to_s
169
-
170
- raise "No such property: #{property}" unless @item.include?(property)
171
-
172
- @item[property]
173
- end
174
-
175
- def []=(property, value)
176
- update_property(property, value)
177
- end
178
-
179
176
  def schedule_reminer(timespec, recipient, done=false, ifopen=false)
180
177
  command_args = ["--send"]
181
178
  command_args << "--recipient=%s" % [ recipient ]
@@ -194,54 +191,5 @@ module Gwtf
194
191
  save
195
192
  end
196
193
  end
197
-
198
- # simple read from the class:
199
- #
200
- # >> i.description
201
- # => "Sample Item"
202
- #
203
- # method like writes:
204
- #
205
- # >> i.description "This is a test"
206
- # => "This is a test"
207
- #
208
- # assignment
209
- #
210
- # >> i.description = "This is a test"
211
- # => "This is a test"
212
- #
213
- # boolean
214
- #
215
- # >> i.description?
216
- # => false
217
- # >> i.description "foo"
218
- # => foo
219
- # >> i.has_description?
220
- # => true
221
- # >> i.has_description
222
- # => true
223
- def method_missing(method, *args)
224
- method = method.to_s
225
-
226
- if @item.include?(method)
227
- if args.empty?
228
- return @item[method]
229
- else
230
- return update_property(method, args.first)
231
- end
232
-
233
- elsif method =~ /^has_(.+?)\?*$/
234
- return !@item[$1].nil?
235
-
236
- elsif method =~ /^(.+)\?$/
237
- return !@item[$1].nil?
238
-
239
- elsif method =~ /^(.+)=$/
240
- property = $1
241
- return update_property(property, args.first) if @item.include?(property)
242
- end
243
-
244
- raise NameError, "undefined local variable or method `#{method}'"
245
- end
246
194
  end
247
195
  end
data/lib/gwtf/items.rb CHANGED
@@ -79,10 +79,14 @@ module Gwtf
79
79
 
80
80
  # Returns an array of total, open and closed items for the current project
81
81
  def stats
82
- count = {"open" => 0, "closed" => 0}
82
+ count = {"open" => 0, "closed" => 0, "overdue" => 0, "due_soon" => 0, "due_today" => 0}
83
83
 
84
84
  each_item do |item|
85
85
  count[ item.status ] += 1
86
+
87
+ count["overdue"] += 1 if item.overdue?
88
+ count["due_soon"] += 1 if item.days_till_due == 1
89
+ count["due_today"] += 1 if item.days_till_due == 0
86
90
  end
87
91
 
88
92
  count["total"] = count["open"] + count["closed"]
@@ -110,7 +114,6 @@ module Gwtf
110
114
  list.puts "Project %s items: %d / %d" % [ @project, count["open"], count["total"] ]
111
115
  list.puts
112
116
  list.puts items.string
113
- list.puts
114
117
 
115
118
  list.string
116
119
  end
data/lib/gwtf/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Gwtf
2
- VERSION = '0.4.4'
2
+ VERSION = '0.4.5'
3
3
  end
data/lib/gwtf.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  module Gwtf
2
+ require 'objhash'
2
3
  require 'gwtf/items'
3
4
  require 'gwtf/item'
4
5
  require 'gwtf/version'
@@ -56,6 +57,30 @@ module Gwtf
56
57
  end
57
58
  end
58
59
 
60
+ def self.green(msg)
61
+ if STDOUT.tty?
62
+ "%s" % [ msg ]
63
+ else
64
+ msg
65
+ end
66
+ end
67
+
68
+ def self.yellow(msg)
69
+ if STDOUT.tty?
70
+ "%s" % [ msg ]
71
+ else
72
+ msg
73
+ end
74
+ end
75
+
76
+ def self.red(msg)
77
+ if STDOUT.tty?
78
+ "%s" % [ msg ]
79
+ else
80
+ msg
81
+ end
82
+ end
83
+
59
84
  # borrowed from ohai, thanks Adam.
60
85
  def self.seconds_to_human(seconds)
61
86
  days = seconds.to_i / 86400
data/lib/objhash.rb ADDED
@@ -0,0 +1,221 @@
1
+ module ObjHash
2
+ include Enumerable
3
+
4
+ module ClassMethods
5
+ # Create a new known property of the ObjHash
6
+ # it's imagined these might have validators, defaults
7
+ # required etc associated with them in args
8
+ def property(name, args={:default => nil, :validation => nil})
9
+ name = name.to_s
10
+
11
+ raise "Already have a property #{name}" if objhash_config.include?(name)
12
+
13
+ objhash_config[name] = {:default => nil, :validation => nil}.merge(args)
14
+ end
15
+
16
+ def objhash_config
17
+ @objhash_values ||= {
18
+ "created_at" => {:default => lambda { Time.now }},
19
+ "edited_at" => {:default => lambda { Time.now }}
20
+ }
21
+ end
22
+
23
+ def objhash_default_value(property)
24
+ property = property.to_s
25
+
26
+ raise "Unknown property #{property}" unless objhash_config.include?(property)
27
+
28
+ if objhash_config[property][:default].is_a?(Proc)
29
+ objhash_config[property][:default].call
30
+ else
31
+ objhash_config[property][:default]
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.included(base)
37
+ base.extend ClassMethods
38
+ end
39
+
40
+ def default_property_value(property)
41
+ self.class.objhash_default_value(property)
42
+ end
43
+
44
+ def objhash_config
45
+ self.class.objhash_config
46
+ end
47
+
48
+ def objhash_values
49
+ return @objhash_values if @objhash_values
50
+
51
+ @objhash_values = {}
52
+
53
+ objhash_config.each_pair do |property, args|
54
+ update_property(property, default_property_value(property))
55
+ end
56
+
57
+ @objhash_values
58
+ end
59
+
60
+ def include?(property)
61
+ objhash_config.include?(property.to_s)
62
+ end
63
+
64
+ def update_property(property, value)
65
+ property = property.to_s
66
+
67
+ raise "Unknown property #{property}" unless include?(property)
68
+
69
+ validate_property(property, value)
70
+
71
+ objhash_values[property] = value
72
+ objhash_values["edited_at"] = Time.now
73
+ end
74
+
75
+ def validate_property(property, value)
76
+ raise "Unknown property #{property}" unless include?(property)
77
+
78
+ validation = objhash_config[property.to_s][:validation]
79
+
80
+ return true if validation.nil?
81
+
82
+ # if the value is the default we dont validate it allowing nil
83
+ # defaults but validation only on assignment of non default value
84
+ return true if value == objhash_config[property.to_s][:default]
85
+
86
+ raise "#{property} should be #{validation}" if value.nil? && !validation.nil?
87
+
88
+ if validation.is_a?(Symbol)
89
+ case validation
90
+ when :boolean
91
+ raise "#{property} should be a boolean" unless [TrueClass, FalseClass].include?(value.class)
92
+ when :ipv6
93
+ begin
94
+ require 'ipaddr'
95
+ ip = IPAddr.new(value)
96
+ raise "#{property} should be a valid IPv6 address" unless ip.ipv6?
97
+ rescue
98
+ raise "#{property} should be a valid IPv6 address"
99
+ end
100
+
101
+ when :ipv4
102
+ begin
103
+ require 'ipaddr'
104
+ ip = IPAddr.new(value)
105
+ raise "#{property} should be a valid IPv4 address" unless ip.ipv4?
106
+ rescue
107
+ raise "#{property} should be a valid IPv4 address"
108
+ end
109
+ else
110
+ raise "Don't know how to validate #{property} using #{validation}"
111
+ end
112
+
113
+ elsif validation.is_a?(Array)
114
+ raise "%s should be one of %s" % [property, validation.join(", ")] unless validation.include?(value)
115
+
116
+ elsif validation.is_a?(Regexp)
117
+ raise "#{property} should match #{validation}" unless value.match(validation)
118
+
119
+ elsif validation.is_a?(Proc)
120
+ raise "#{property} does not validate against lambda" unless validation.call(value)
121
+
122
+ else
123
+ raise "#{property} is a #{value.class} should be a #{validation}" unless value.is_a?(validation)
124
+ end
125
+
126
+ return true
127
+ end
128
+
129
+ def to_hash
130
+ objhash_values
131
+ end
132
+
133
+ def to_s
134
+ objhash_values.inspect
135
+ end
136
+
137
+ def to_json
138
+ objhash_values.to_json
139
+ end
140
+
141
+ def to_yaml
142
+ objhash_values.to_yaml
143
+ end
144
+
145
+ def each
146
+ objhash_values.keys.sort.each do |property|
147
+ yield [property, objhash_values[property]]
148
+ end
149
+ end
150
+
151
+ def [](property)
152
+ raise "No such property: #{property}" unless include?(property)
153
+
154
+ objhash_values[property.to_s]
155
+ end
156
+
157
+ def []=(property, value)
158
+ update_property(property, value)
159
+ end
160
+
161
+ def merge(hsh)
162
+ objhash_values.merge(hsh)
163
+ end
164
+
165
+ def merge!(hsh)
166
+ raise TypeError, "Can't convert #{hsh.class} into Hash" unless hsh.respond_to?(:to_hash)
167
+
168
+ objhash_values.keys.each do |k|
169
+ next if ["edited_at", "created_at"].include?(k)
170
+ update_property(k, hsh[k]) if hsh.include?(k)
171
+ end
172
+
173
+ self
174
+ end
175
+
176
+ # simple read from the class:
177
+ #
178
+ # >> i.description
179
+ # => "Sample Item"
180
+ #
181
+ # method like writes:
182
+ #
183
+ # >> i.description "This is a test"
184
+ # => "This is a test"
185
+ #
186
+ # assignment
187
+ #
188
+ # >> i.description = "This is a test"
189
+ # => "This is a test"
190
+ #
191
+ # boolean
192
+ #
193
+ # >> i.description?
194
+ # => false
195
+ # >> i.description "foo"
196
+ # => foo
197
+ # >> i.has_description?
198
+ # => true
199
+ # >> i.has_description
200
+ # => true
201
+ def method_missing(method, *args)
202
+ method = method.to_s
203
+
204
+ if include?(method)
205
+ if args.empty?
206
+ return objhash_values[method]
207
+ else
208
+ return update_property(method, args.first)
209
+ end
210
+
211
+ elsif method =~ /^(has_)*(.+?)\?$/
212
+ return !!objhash_values[$2]
213
+
214
+ elsif method =~ /^(.+)=$/
215
+ property = $1
216
+ return update_property(property, args.first) if include?(property)
217
+ end
218
+
219
+ raise NameError, "undefined local variable or method `#{method}'"
220
+ end
221
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gwtf
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 5
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 4
9
- - 4
10
- version: 0.4.4
9
+ - 5
10
+ version: 0.4.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - R.I.Pienaar
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-04-04 00:00:00 +01:00
18
+ date: 2012-04-07 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -102,6 +102,7 @@ extra_rdoc_files: []
102
102
 
103
103
  files:
104
104
  - bin/gwtf
105
+ - lib/objhash.rb
105
106
  - lib/gwtf/commands/show_command.rb
106
107
  - lib/gwtf/commands/open_command.rb
107
108
  - lib/gwtf/commands/shell_command.rb