gwtf 0.4.4 → 0.4.5

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