one_inch_punch 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,12 @@
1
+ == 0.3.0 2008-04-05
2
+
3
+ * 1 minor enhancement:
4
+ * Can now have 'sub-projects', or projects with names of the form 'parent/child'
5
+ * A parent will be checked in if a child is checked in
6
+ * Punching out of a parent project will punch the real checked-in project out
7
+ * Listing time and getting totals for a parent project will include child time
8
+ * Some small convenience methods probably nobody cares about, so they aren't included in the total number of enchancements above
9
+
1
10
  == 0.2.4 2008-12-17
2
11
 
3
12
  * 1 tiny enhancement:
data/README.txt CHANGED
@@ -12,13 +12,8 @@ One-inch punch: Smaller, more effective
12
12
  == FEATURES/PROBLEMS:
13
13
 
14
14
  * Can load and write .punch.yml data compatibly with Ara's punch gem
15
- * Can punch in and out of projects (including creating a project by punching in)
16
- * Can query project status
17
- * Can delete a project
18
- * Can list project data
19
- * Can give total time for a project
20
- * Can be used command-line
21
- * Command-line output is less ugly
15
+ * Things you may expect from a time-tracking program, like punching in and out and getting time data
16
+ * The ability to be punched in to multiple projects at once, because double-billing is awesome
22
17
  * More, since this is unfinished
23
18
 
24
19
  == SYNOPSIS:
data/bin/punch CHANGED
@@ -66,10 +66,10 @@ commands = {
66
66
  end,
67
67
  'total' => lambda do |project|
68
68
  result = Punch.total(project, OPTIONS.merge(:format => true))
69
- if project
70
- puts result.inspect
71
- else
69
+ if result.is_a?(Hash)
72
70
  puts result.to_yaml
71
+ else
72
+ puts result.inspect
73
73
  end
74
74
  end,
75
75
  'in' => lambda do |project|
data/lib/punch.rb CHANGED
@@ -45,14 +45,19 @@ class Punch
45
45
  project = nil
46
46
  end
47
47
 
48
- return data.keys.inject({}) { |hash, project| hash.merge(project => status(project, options)) } unless project
48
+ return projects.inject({}) { |hash, project| hash.merge(project => status(project, options)) } unless project
49
49
 
50
50
  project_data = data[project]
51
- return nil if !project_data or project_data.empty?
51
+ time_data = (project_data || []).last
52
+
53
+ if time_data
54
+ status = time_data['out'] ? 'out' : 'in'
55
+ end
56
+
57
+ status, time_data = check_child_status(project, status, time_data)
52
58
 
53
- time_data = project_data.last
54
- status = time_data['out'] ? 'out' : 'in'
55
59
  return status unless options[:full]
60
+ return status if status.nil?
56
61
 
57
62
  { :status => status, :time => time_data[status] }
58
63
  end
@@ -81,7 +86,7 @@ class Punch
81
86
  if project
82
87
  return false unless do_out_single(project, options)
83
88
  else
84
- return false unless data.keys.collect { |project| do_out_single(project, options) }.any?
89
+ return false unless projects.collect { |project| do_out_single(project, options) }.any?
85
90
  end
86
91
  true
87
92
  end
@@ -94,10 +99,17 @@ class Punch
94
99
  def list(*args)
95
100
  options = args.last.is_a?(Hash) ? args.pop : {}
96
101
  project = args.first
102
+
97
103
  if project
98
- do_list_single(project, options)
104
+ list_projects = child_projects(project) + [project]
99
105
  else
100
- data.keys.inject({}) { |hash, project| hash.merge(project => do_list_single(project, options)) }
106
+ list_projects = projects
107
+ end
108
+
109
+ if list_projects.length == 1
110
+ do_list_single(list_projects.first, options)
111
+ else
112
+ list_projects.inject({}) { |hash, project| hash.merge(project => do_list_single(project, options)) }
101
113
  end
102
114
  end
103
115
 
@@ -127,6 +139,7 @@ class Punch
127
139
 
128
140
  def do_out_single(project, options)
129
141
  return false if out?(project)
142
+ project = in_child(project) || project
130
143
  time = time_from_options(options)
131
144
  log(project, options[:message], :time => time) if options[:message]
132
145
  log(project, 'punch out', :time => time)
@@ -149,5 +162,29 @@ class Punch
149
162
  def time_from_options(options)
150
163
  options[:time] || options[:at] || Time.now
151
164
  end
165
+
166
+ def projects
167
+ data.keys
168
+ end
169
+
170
+ def child_projects(project)
171
+ projects.select { |proj| proj.match(/^#{Regexp.escape(project)}/) } - [project]
172
+ end
173
+
174
+ def in_child(project)
175
+ child_projects(project).detect { |proj| status(proj) == 'in' }
176
+ end
177
+
178
+ def check_child_status(project, status, time_data)
179
+ if status != 'in'
180
+ in_child = in_child(project)
181
+ if in_child
182
+ status = 'in'
183
+ time_data = data[in_child].last
184
+ end
185
+ end
186
+
187
+ return status, time_data
188
+ end
152
189
  end
153
190
  end
@@ -36,4 +36,13 @@ class Punch
36
36
  def log(message, options = {})
37
37
  self.class.log(project, message, options)
38
38
  end
39
+
40
+ def ==(other)
41
+ project == other.project
42
+ end
43
+
44
+ def child_projects
45
+ Punch.send(:child_projects, project).collect { |proj| Punch.new(proj) }
46
+ end
47
+ alias_method :children, :child_projects
39
48
  end
data/lib/punch/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  class Punch
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 2
5
- TINY = 4
4
+ MINOR = 3
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -64,11 +64,11 @@ describe 'punch command' do
64
64
  run_command('total', @project)
65
65
  end
66
66
 
67
- it 'should output the total as YAML if no project given' do
68
- result = 'total data'
67
+ it 'should output the total as YAML if the total data is a Hash' do
68
+ result = { :some_project => 'total data', :some_other_project => 'other total data' }
69
69
  Punch.stub!(:total).and_return(result)
70
70
  self.should.receive(:puts).with(result.to_yaml)
71
- run_command('total')
71
+ run_command('total', @project)
72
72
  end
73
73
 
74
74
  it 'should not write the data' do
@@ -187,7 +187,7 @@ describe 'punch command' do
187
187
  run_command('status', @project, '--full')
188
188
  end
189
189
 
190
- it 'should output the status as YAML if not project given even if a full option is given' do
190
+ it 'should output the status as YAML if no project given even if a full option is given' do
191
191
  result = 'status data'
192
192
  Punch.stub!(:status).and_return(result)
193
193
  self.should.receive(:puts).with(result.to_yaml)
@@ -381,4 +381,52 @@ describe Punch, 'instance' do
381
381
  @punch.log(@message).should == @log
382
382
  end
383
383
  end
384
+
385
+ describe 'equality' do
386
+ it 'should be equal to another instance for the same project' do
387
+ Punch.new('proj').should == Punch.new('proj')
388
+ end
389
+
390
+ it 'should not be equal to an instance for a different project' do
391
+ Punch.new('proj').should.not == Punch.new('other')
392
+ end
393
+ end
394
+
395
+ it 'should return child projects' do
396
+ @punch.should.respond_to(:child_projects)
397
+ end
398
+
399
+ describe 'returning child projects' do
400
+ before do
401
+ Punch.instance_eval do
402
+ class << self
403
+ public :data, :data=
404
+ end
405
+ end
406
+
407
+ @projects = {}
408
+ @projects['parent'] = 'daddy'
409
+ @projects['child'] = @projects['parent'] + '/sugar'
410
+ @projects['kid'] = @projects['parent'] + '/object'
411
+
412
+ @data = { @projects['parent'] => [], @projects['child'] => [], @projects['kid'] => [] }
413
+ Punch.data = @data
414
+ end
415
+
416
+ it 'should return instances for each child project' do
417
+ children = Punch.new(@projects['parent']).child_projects
418
+ expected = [Punch.new(@projects['child']), Punch.new(@projects['kid'])]
419
+ children.sort_by { |c| c.project }.should == expected.sort_by { |e| e.project }
420
+ end
421
+
422
+ it "should provide 'children' as an alias" do
423
+ children = Punch.new(@projects['parent']).children
424
+ expected = [Punch.new(@projects['child']), Punch.new(@projects['kid'])]
425
+ children.sort_by { |c| c.project }.should == expected.sort_by { |e| e.project }
426
+ end
427
+
428
+ it 'should return an empty array if the project has no child projects' do
429
+ @punch.child_projects.should == []
430
+ end
431
+ end
384
432
  end
data/spec/punch_spec.rb CHANGED
@@ -242,6 +242,115 @@ describe Punch do
242
242
  }
243
243
  end
244
244
  end
245
+
246
+ describe 'handling a sub-project' do
247
+ before do
248
+ @projects['parent'] = 'daddy'
249
+ @projects['child'] = @projects['parent'] + '/sugar'
250
+ end
251
+
252
+ it "should return 'in' for a non-existent parent project if the sub-project is punched in" do
253
+ @data[@projects['child']] = [ { 'in' => @now } ]
254
+ Punch.data = @data
255
+ Punch.status(@projects['parent']).should == 'in'
256
+ end
257
+
258
+ it "should return 'in' for an empty parent project if the sub-project is punched in" do
259
+ @data[@projects['parent']] = []
260
+ @data[@projects['child']] = [ { 'in' => @now } ]
261
+ Punch.data = @data
262
+ Punch.status(@projects['parent']).should == 'in'
263
+ end
264
+
265
+ it "should return 'in' for a punched-out parent project if the sub-project is punched in" do
266
+ @data[@projects['parent']] = [ { 'in' => @now - 13, 'out' => @now - 5 } ]
267
+ @data[@projects['child']] = [ { 'in' => @now } ]
268
+ Punch.data = @data
269
+ Punch.status(@projects['parent']).should == 'in'
270
+ end
271
+
272
+ it "should use the sub-project's punch-in time for the parent project when returning full status" do
273
+ @data[@projects['child']] = [ { 'in' => @now } ]
274
+ Punch.data = @data
275
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'in', :time => @now }
276
+
277
+ @data[@projects['parent']] = []
278
+ Punch.data = @data
279
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'in', :time => @now }
280
+
281
+ @data[@projects['parent']] = [ { 'in' => @now - 13, 'out' => @now - 5 } ]
282
+ Punch.data = @data
283
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'in', :time => @now }
284
+ end
285
+
286
+ it "should return nil for a non-existent parent project if the sub-project does not exist" do
287
+ Punch.status(@projects['parent']).should.be.nil
288
+ end
289
+
290
+ it "should return nil for an empty parent project if the sub-project does not exist" do
291
+ @data[@projects['parent']] = []
292
+ Punch.data = @data
293
+ Punch.status(@projects['parent']).should.be.nil
294
+ end
295
+
296
+ it "should return nil for a non-existent parent project if the sub-project is empty" do
297
+ @data[@projects['child']] = []
298
+ Punch.data = @data
299
+ Punch.status(@projects['parent']).should.be.nil
300
+ end
301
+
302
+ it "should return nil for an empty parent project if the sub-project is empty" do
303
+ @data[@projects['parent']] = []
304
+ @data[@projects['child']] = []
305
+ Punch.data = @data
306
+ Punch.status(@projects['parent']).should.be.nil
307
+ end
308
+
309
+ it "should return nil for the parent project when returning full status" do
310
+ Punch.status(@projects['parent'], :full => true).should.be.nil
311
+
312
+ @data[@projects['parent']] = []
313
+ Punch.data = @data
314
+ Punch.status(@projects['parent'], :full => true).should.be.nil
315
+
316
+ @data.delete(@projects['parent'])
317
+ @data[@projects['child']] = []
318
+ Punch.data = @data
319
+ Punch.status(@projects['parent'], :full => true).should.be.nil
320
+
321
+ @data[@projects['parent']] = []
322
+ @data[@projects['child']] = []
323
+ Punch.data = @data
324
+ Punch.status(@projects['parent'], :full => true).should.be.nil
325
+ end
326
+
327
+ it "should return 'out' for a punched-out parent project if the sub-project does not exist" do
328
+ @data[@projects['parent']] = [ { 'in' => @now - 13, 'out' => @now - 5 } ]
329
+ Punch.data = @data
330
+ Punch.status(@projects['parent']).should == 'out'
331
+ end
332
+
333
+ it "should return 'out' for a punched-out parent project if the sub-project is empty" do
334
+ @data[@projects['parent']] = [ { 'in' => @now - 13, 'out' => @now - 5 } ]
335
+ @data[@projects['child']] = []
336
+ Punch.data = @data
337
+ Punch.status(@projects['parent']).should == 'out'
338
+ end
339
+
340
+ it "should use the parent project's punch-out time for the parent project when returning full status" do
341
+ @data[@projects['parent']] = [ { 'in' => @now - 13, 'out' => @now - 5 } ]
342
+ Punch.data = @data
343
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'out', :time => @now - 5 }
344
+
345
+ @data[@projects['child']] = []
346
+ Punch.data = @data
347
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'out', :time => @now - 5 }
348
+
349
+ @data[@projects['child']] = [ { 'in' => @now - 4, 'out' => @now - 1 } ]
350
+ Punch.data = @data
351
+ Punch.status(@projects['parent'], :full => true).should == { :status => 'out', :time => @now - 5 }
352
+ end
353
+ end
245
354
  end
246
355
 
247
356
  it 'should indicate whether a project is punched out' do
@@ -674,6 +783,45 @@ describe Punch do
674
783
  end
675
784
  end
676
785
  end
786
+
787
+ describe 'handling a sub-project' do
788
+ before do
789
+ @projects = {}
790
+ @projects['parent'] = 'daddy'
791
+ @projects['child'] = @projects['parent'] + '/sugar'
792
+ end
793
+
794
+ it 'should actually punch out the sub-project when told to punch out the parent project' do
795
+ @data[@projects['parent']] = [ { 'in' => @now - 100, 'out' => @now - 50 } ]
796
+ @data[@projects['child']] = [ { 'in' => @now - 20 } ]
797
+ Punch.data = @data
798
+ Punch.out(@projects['parent'])
799
+ Punch.data[@projects['child']].last['out'].should == @now
800
+ end
801
+
802
+ it 'should not change the punch-out time for the parent project' do
803
+ @data[@projects['parent']] = [ { 'in' => @now - 100, 'out' => @now - 50 } ]
804
+ @data[@projects['child']] = [ { 'in' => @now - 20 } ]
805
+ Punch.data = @data
806
+ Punch.out(@projects['parent'])
807
+ Punch.data[@projects['parent']].last['out'].should == @now - 50
808
+ end
809
+
810
+ it 'should not add data for a non-existent parent project' do
811
+ @data[@projects['child']] = [ { 'in' => @now - 20 } ]
812
+ Punch.data = @data
813
+ Punch.out(@projects['parent'])
814
+ Punch.data[@projects['parent']].should.be.nil
815
+ end
816
+
817
+ it 'should not add data for an empty parent project' do
818
+ @data[@projects['parent']] = []
819
+ @data[@projects['child']] = [ { 'in' => @now - 20 } ]
820
+ Punch.data = @data
821
+ Punch.out(@projects['parent'])
822
+ Punch.data[@projects['parent']].should == []
823
+ end
824
+ end
677
825
  end
678
826
 
679
827
  it 'should delete a project' do
@@ -830,6 +978,41 @@ describe Punch do
830
978
  Punch.data.should == old_data
831
979
  end
832
980
  end
981
+
982
+ describe 'handling a sub-project' do
983
+ before do
984
+ @projects = {}
985
+ @projects['parent'] = 'daddy'
986
+ @projects['child'] = @projects['parent'] + '/sugar'
987
+ @data[@projects['parent']] = [ { 'in' => @now - 100, 'out' => @now - 50 } ]
988
+ @data[@projects['child']] = [ { 'in' => @now - 20 } ]
989
+ Punch.data = @data
990
+ end
991
+
992
+ it 'should return data for the parent and sub-project' do
993
+ list_data = { @projects['parent'] => @data[@projects['parent']], @projects['child'] => @data[@projects['child']] }
994
+ Punch.list(@projects['parent']).should == list_data
995
+ end
996
+
997
+ it 'should respect options' do
998
+ list_data = { @projects['parent'] => [], @projects['child'] => @data[@projects['child']] }
999
+ Punch.list(@projects['parent'], :after => @now - 21).should == list_data
1000
+ end
1001
+
1002
+ describe 'when do project is given' do
1003
+ before do
1004
+ @projects = ['test project', 'out project', 'other project']
1005
+ @data[@projects[0]] = [ {'in' => @now - 50, 'out' => @now - 25} ]
1006
+ @data[@projects[1]] = [ {'in' => @now - 300, 'out' => @now - 250}, {'in' => @now - 40, 'out' => @now - 20} ]
1007
+ @data[@projects[2]] = [ {'in' => @now - 50, 'out' => @now - 35} ]
1008
+ Punch.data = @data
1009
+ end
1010
+
1011
+ it 'should return data for all projects' do
1012
+ Punch.list.should == @data
1013
+ end
1014
+ end
1015
+ end
833
1016
  end
834
1017
 
835
1018
  it 'should get the total time for a project' do
@@ -930,15 +1113,51 @@ describe Punch do
930
1113
  end
931
1114
 
932
1115
  it 'should give totals for all projects' do
933
- Punch.total.should == { @projects[0] => 25, @projects[1] => 70, @projects[2] => 15}
1116
+ Punch.total.should == { @projects[0] => 25, @projects[1] => 70, @projects[2] => 15 }
934
1117
  end
935
1118
 
936
1119
  it 'should respect options' do
937
- Punch.total(:after => @now - 51).should == { @projects[0] => 25, @projects[1] => 20, @projects[2] => 15}
1120
+ Punch.total(:after => @now - 51).should == { @projects[0] => 25, @projects[1] => 20, @projects[2] => 15 }
938
1121
  end
939
1122
 
940
1123
  it 'should format the time spent if passed a format option' do
941
- Punch.total(:format => true).should == { @projects[0] => "00:25", @projects[1] => "01:10", @projects[2] => "00:15"}
1124
+ Punch.total(:format => true).should == { @projects[0] => "00:25", @projects[1] => "01:10", @projects[2] => "00:15" }
1125
+ end
1126
+ end
1127
+
1128
+ describe 'handling a sub-project' do
1129
+ before do
1130
+ @projects = {}
1131
+ @projects['parent'] = 'daddy'
1132
+ @projects['child'] = @projects['parent'] + '/sugar'
1133
+ @data[@projects['parent']] = [ { 'in' => @now - 100, 'out' => @now - 50 } ]
1134
+ @data[@projects['child']] = [ { 'in' => @now - 20, 'out' => @now - 10 } ]
1135
+ Punch.data = @data
1136
+ end
1137
+
1138
+ it 'should return data for the parent and sub-project' do
1139
+ total_data = { @projects['parent'] => 50, @projects['child'] => 10 }
1140
+ Punch.total(@projects['parent']).should == total_data
1141
+ end
1142
+
1143
+ it 'should respect options' do
1144
+ total_data = { @projects['parent'] => 0, @projects['child'] => 10 }
1145
+ Punch.total(@projects['parent'], :after => @now - 21).should == total_data
1146
+ end
1147
+
1148
+ describe 'when do project is given' do
1149
+ before do
1150
+ @extra_projects = ['test project', 'out project', 'other project']
1151
+ @data[@extra_projects[0]] = [ {'in' => @now - 50, 'out' => @now - 25} ]
1152
+ @data[@extra_projects[1]] = [ {'in' => @now - 300, 'out' => @now - 250}, {'in' => @now - 40, 'out' => @now - 20} ]
1153
+ @data[@extra_projects[2]] = [ {'in' => @now - 50, 'out' => @now - 35} ]
1154
+ Punch.data = @data
1155
+ end
1156
+
1157
+ it 'should give totals for all projects' do
1158
+ total_data = { @extra_projects[0] => 25, @extra_projects[1] => 70, @extra_projects[2] => 15, @projects['parent'] => 50, @projects['child'] => 10 }
1159
+ Punch.total.should == total_data
1160
+ end
942
1161
  end
943
1162
  end
944
1163
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: one_inch_punch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yossef Mendelssohn
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-12-17 00:00:00 -06:00
12
+ date: 2009-04-05 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency