timesheet 0.2.0

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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/timesheet.rb'}"
9
+ puts "Loading timesheet gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe Range do
4
+
5
+ it "should detect overlaps" do
6
+ (1..5).overlap?(4..8).should be_true
7
+ (1...5).overlap?(5..10).should be_false
8
+ end
9
+
10
+ it "should allow include?" do
11
+ (1..5).include?(2..3).should be_true
12
+ (1..5).include?(4..8).should be_false
13
+ (1..5).include?(3).should be_true
14
+ (1..5).include?(6).should be_false
15
+ end
16
+
17
+ end
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+
11
+ require 'timesheet'
12
+
13
+
14
+ #Spec::Runner.configure do |config|
15
+ # config.mock_with :mocha
16
+ #end
17
+
@@ -0,0 +1,137 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe TimeEntry do
4
+
5
+ context "Construction" do
6
+
7
+ it "should allow initialization of fields" do
8
+ lambda { TimeEntry.new("Project", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0)) }.should_not raise_error()
9
+ end
10
+
11
+ it "should raise ArgumentError if end time is before start time" do
12
+ lambda { TimeEntry.new("Project", Time.local(2009, 12, 1, 17, 0, 0), Time.local(2009, 12, 1, 9, 0, 0)) }.should raise_error(ArgumentError)
13
+ end
14
+
15
+ end
16
+
17
+ context "Basic Features" do
18
+
19
+ before :each do
20
+ @time_entry = TimeEntry.new("Project", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
21
+ end
22
+
23
+
24
+ it "should know its initial project" do
25
+ @time_entry.project.should == "Project"
26
+ end
27
+
28
+ it "should have an know its initial start time" do
29
+ @time_entry.start_time.should == Time.local(2009, 12, 1, 9, 0, 0)
30
+ end
31
+
32
+ it "should know its initial end time" do
33
+ @time_entry.end_time.should == Time.local(2009, 12, 1, 17, 0, 0)
34
+ end
35
+
36
+ it "should have an initial comment of nil" do
37
+ @time_entry.comment.should == nil
38
+ end
39
+
40
+ it "should calculate its duration from start and end times" do
41
+ @time_entry.duration.should == 8.hours
42
+ end
43
+
44
+ it "should remember its assigned project" do
45
+ @time_entry.project = "Project2"
46
+ @time_entry.project.should == "Project2"
47
+ end
48
+
49
+ it "should raise an ArgumentError if a start_time is assigned that is after the end_time" do
50
+ lambda { @time_entry.start_time = Time.local(2009, 12, 1, 18, 0 , 0) }.should raise_error(ArgumentError)
51
+ end
52
+
53
+ it "should raise an ArgumentError if an end_time is assigned that is before the start_time" do
54
+ lambda { @time_entry.end_time = Time.local(2009, 12, 1, 8, 0 , 0) }.should raise_error(ArgumentError)
55
+ end
56
+
57
+ it "should remember its assigned start time" do
58
+ time = Time.local(2009, 12, 1, 12, 0, 0)
59
+ @time_entry.start_time = time
60
+ @time_entry.start_time.should == time
61
+ end
62
+
63
+ it "should remember its assigned end time" do
64
+ time = Time.local(2009, 12, 1, 12, 0, 0)
65
+ @time_entry.end_time = time
66
+ @time_entry.end_time.should == time
67
+ end
68
+
69
+ it "should remember its assigned comment" do
70
+ @time_entry.comment = "Comment"
71
+ @time_entry.comment.should == "Comment"
72
+ end
73
+
74
+ it "should not allow assignment to duration" do
75
+ @time_entry.should_not respond_to :duration=
76
+ end
77
+
78
+ end
79
+
80
+ context "comparison and sorting" do
81
+ before :each do
82
+ @early_entry = TimeEntry.new("Project", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 12, 0, 0))
83
+ @late_entry = TimeEntry.new("Project", Time.local(2009, 12, 1, 13, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
84
+ end
85
+
86
+ it "should be comparable to other time entries" do
87
+ @early_entry.should respond_to :<=>
88
+ end
89
+
90
+ it "should order before an entry with a later start time" do
91
+ lambda { @early_entry < @late_entry}.should be_true
92
+ end
93
+
94
+ it "should order after an entry with an earlier start time" do
95
+ lambda { @late_entry > @early_entry}.should be_true
96
+ end
97
+ end
98
+
99
+ context "Conflict detection" do
100
+
101
+ it "these should not conflict" do
102
+ entry1 = TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
103
+ entry2 = TimeEntry.new("Project2", Time.local(2009, 12, 2, 9, 0, 0), Time.local(2009, 12, 2, 17, 0, 0))
104
+
105
+ entry1.conflict?(entry2).should be_false
106
+ entry2.conflict?(entry1).should be_false
107
+ end
108
+
109
+
110
+ it "should conflict" do
111
+ entry1 = TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
112
+ entry2 = TimeEntry.new("Project2", Time.local(2009, 12, 1, 12, 0, 0), Time.local(2009, 12, 1, 20, 0, 0))
113
+
114
+ entry1.conflict?(entry2).should be_true
115
+ entry2.conflict?(entry1).should be_true
116
+ end
117
+
118
+ it "should conflict" do
119
+ entry1 = TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
120
+ entry2 = TimeEntry.new("Project2", Time.local(2009, 12, 1, 12, 0, 0), Time.local(2009, 12, 1, 14, 0, 0))
121
+
122
+ entry1.conflict?(entry2).should be_true
123
+ entry2.conflict?(entry1).should be_true
124
+ end
125
+
126
+ it "should not conflict if its start time is the same as another entries end time" do
127
+ entry1 = TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 12, 0, 0))
128
+ entry2 = TimeEntry.new("Project2", Time.local(2009, 12, 1, 12, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
129
+
130
+ entry1.conflict?(entry2).should be_false
131
+ entry2.conflict?(entry1).should be_false
132
+
133
+ end
134
+
135
+ end
136
+
137
+ end
@@ -0,0 +1,164 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ TEST_FILE = "/var/tmp/timesheet_test.pstore"
4
+
5
+ describe TimeLog do
6
+
7
+ after :all do
8
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
9
+ end
10
+
11
+ context "adding items" do
12
+
13
+ before :all do
14
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
15
+ @store = YAML::Store.new(TEST_FILE)
16
+ @time_log = TimeLog.new(@store)
17
+ end
18
+
19
+
20
+ it "should have no entries initially" do
21
+ @time_log.count.should == 0
22
+ end
23
+
24
+ it "should allow time entries to be added" do
25
+ t = Time.now
26
+ entry = TimeEntry.new("Project1", t.hours_ago(2), t)
27
+ entry.record_number.should == nil
28
+ @time_log.add entry
29
+ @time_log.count.should == 1
30
+ entry.record_number.should == 1
31
+ end
32
+ end
33
+
34
+ context "finding items" do
35
+
36
+ before :all do
37
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
38
+ @store = YAML::Store.new(TEST_FILE)
39
+ @time_log = TimeLog.new(@store)
40
+
41
+ @time_log.add TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
42
+ @time_log.add TimeEntry.new("Project2", Time.local(2009, 12, 2, 9, 0, 0), Time.local(2009, 12, 2, 17, 0, 0))
43
+ @time_log.add TimeEntry.new("Project3", Time.local(2009, 12, 3, 9, 0, 0), Time.local(2009, 12, 3, 17, 0, 0))
44
+ @time_log.add TimeEntry.new("Project4", Time.local(2009, 12, 4, 9, 0, 0), Time.local(2009, 12, 4, 17, 0, 0))
45
+ @time_log.add TimeEntry.new("Project5", Time.local(2009, 12, 5, 9, 0, 0), Time.local(2009, 12, 5, 17, 0, 0))
46
+ end
47
+
48
+
49
+ it "should raise an error if attempting to find with an unknown index" do
50
+ lambda{@time_log.find(100)}.should raise_error(ArgumentError)
51
+ @time_log.count.should == 5
52
+ end
53
+
54
+ it "should not allow finding by record number" do
55
+ entry = @time_log.find(2)
56
+ entry.project.should eql("Project2")
57
+ end
58
+
59
+ end
60
+
61
+ context "deleting items" do
62
+
63
+ before :all do
64
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
65
+ @store = YAML::Store.new(TEST_FILE)
66
+ @time_log = TimeLog.new(@store)
67
+
68
+ @time_log.add TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
69
+ @time_log.add TimeEntry.new("Project2", Time.local(2009, 12, 2, 9, 0, 0), Time.local(2009, 12, 2, 17, 0, 0))
70
+ @time_log.add TimeEntry.new("Project3", Time.local(2009, 12, 3, 9, 0, 0), Time.local(2009, 12, 3, 17, 0, 0))
71
+ @time_log.add TimeEntry.new("Project4", Time.local(2009, 12, 4, 9, 0, 0), Time.local(2009, 12, 4, 17, 0, 0))
72
+ @time_log.add TimeEntry.new("Project5", Time.local(2009, 12, 5, 9, 0, 0), Time.local(2009, 12, 5, 17, 0, 0))
73
+ end
74
+
75
+
76
+ it "should raise an error if attempting to delete an unknown index" do
77
+ lambda{@time_log.delete(100)}.should raise_error(ArgumentError)
78
+ @time_log.count.should == 5
79
+ end
80
+
81
+ it "should not allow conflicting entries to be added" do
82
+ conflicting_entry = TimeEntry.new("Conflicting", Time.local(2009, 12, 2, 12, 0, 0), Time.local(2009, 12, 2, 13, 0, 0))
83
+ lambda { @time_log.add conflicting_entry }.should raise_error(ArgumentError)
84
+ @time_log.count.should == 5
85
+ end
86
+
87
+ end
88
+
89
+ context "editing items" do
90
+
91
+ before :all do
92
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
93
+ @store = YAML::Store.new(TEST_FILE)
94
+ @time_log = TimeLog.new(@store)
95
+
96
+ @time_log.add TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
97
+ @time_log.add TimeEntry.new("Project2", Time.local(2009, 12, 2, 9, 0, 0), Time.local(2009, 12, 2, 17, 0, 0))
98
+ @time_log.add TimeEntry.new("Project3", Time.local(2009, 12, 3, 9, 0, 0), Time.local(2009, 12, 3, 17, 0, 0))
99
+ @time_log.add TimeEntry.new("Project4", Time.local(2009, 12, 4, 9, 0, 0), Time.local(2009, 12, 4, 17, 0, 0))
100
+ @time_log.add TimeEntry.new("Project5", Time.local(2009, 12, 5, 9, 0, 0), Time.local(2009, 12, 5, 17, 0, 0))
101
+ end
102
+
103
+ it "should notify with an error if the edited item's index cannot be found" do
104
+ properties = { :project => "New Project" }
105
+ lambda{@time_log.update(100, properties)}.should raise_error(ArgumentError)
106
+ end
107
+
108
+ it "should allow updating of existing items" do
109
+ properties = {:project => "some new project"}
110
+ @time_log.update(2, properties)
111
+ @time_log.count.should == 5
112
+
113
+ entry = @time_log.find(2)
114
+ entry.project.should eql("some new project")
115
+ end
116
+
117
+ it "should allow updating of existing item start time" do
118
+ properties = {:start => Time.local(2009, 12, 2, 7, 0, 0)}
119
+ @time_log.update(2, properties)
120
+ @time_log.count.should == 5
121
+
122
+ entry = @time_log.find(2)
123
+ entry.start_time.should eql( Time.local(2009, 12, 2, 7, 0, 0))
124
+ end
125
+
126
+ it "should notify with an error if the new times will overlap with existing items" do
127
+ properties = {:end => Time.local(2009, 12, 3, 10, 0, 0)}
128
+ lambda{@time_log.update(2, properties)}.should raise_error(ArgumentError)
129
+ end
130
+
131
+ end
132
+
133
+ context "extracting items" do
134
+
135
+ before :all do
136
+ File.delete(TEST_FILE) if File.exists?(TEST_FILE)
137
+ @store = YAML::Store.new(TEST_FILE)
138
+ @time_log = TimeLog.new(@store)
139
+
140
+ @time_log.add TimeEntry.new("Project1", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 17, 0, 0))
141
+ @time_log.add TimeEntry.new("Project2", Time.local(2009, 12, 2, 9, 0, 0), Time.local(2009, 12, 2, 17, 0, 0))
142
+ @time_log.add TimeEntry.new("Project3", Time.local(2009, 12, 3, 9, 0, 0), Time.local(2009, 12, 3, 17, 0, 0))
143
+ @time_log.add TimeEntry.new("Project4", Time.local(2009, 12, 4, 9, 0, 0), Time.local(2009, 12, 4, 17, 0, 0))
144
+ @time_log.add TimeEntry.new("Project5", Time.local(2009, 12, 5, 9, 0, 0), Time.local(2009, 12, 5, 17, 0, 0))
145
+ end
146
+
147
+ it "should return an empty array if no items are in the range" do
148
+ entries = @time_log.extract_entries( Time.local(2009, 11, 1, 9, 0, 0), Time.local(2009, 11, 10, 17, 0, 0))
149
+ entries.should be_empty
150
+ end
151
+
152
+ it "should return a collection of items that are covered by the range" do
153
+ entries = @time_log.extract_entries( Time.local(2009, 12, 2, 12, 0, 0), Time.local(2009, 12, 4, 12, 0, 0))
154
+ entries.count.should == 3
155
+ end
156
+
157
+ it "should notify with an error if the new times will overlap with existing items" do
158
+ properties = {:end => Time.local(2009, 12, 3, 10, 0, 0)}
159
+ lambda{@time_log.update(2, properties)}.should raise_error(ArgumentError)
160
+ end
161
+
162
+ end
163
+
164
+ end
@@ -0,0 +1,104 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe TimeReport do
4
+
5
+ before :each do
6
+ @entries =
7
+ [TimeEntry.new("ProjectA", Time.local(2009, 12, 1, 9, 0, 0), Time.local(2009, 12, 1, 12, 0, 0), "comment 1"),
8
+ TimeEntry.new("ProjectB", Time.local(2009, 12, 1, 1, 0, 0), Time.local(2009, 12, 1, 17, 0, 0), "comment 2"),
9
+ TimeEntry.new("ProjectA", Time.local(2009, 12, 3, 9, 0, 0), Time.local(2009, 12, 3, 17, 0, 0), "comment 3"),
10
+ TimeEntry.new("ProjectB", Time.local(2009, 12, 4, 9, 0, 0), Time.local(2009, 12, 4, 17, 0, 0), "comment 4"),
11
+ TimeEntry.new("ProjectC", Time.local(2009, 12, 5, 9, 0, 0), Time.local(2009, 12, 5, 17, 0, 0), "comment 5")]
12
+ @time_report = TimeReport.new(@entries)
13
+ end
14
+
15
+ it "should be able to trim its entries to a specific time span" do
16
+ @time_report.trim(Time.local(2009, 12, 1, 11, 0, 0), Time.local(2009, 12, 5, 12, 0, 0))
17
+ @time_report.entries[0].start_time.should eql(Time.local(2009, 12, 1, 11, 0, 0))
18
+ @time_report.entries[-1].end_time.should eql(Time.local(2009, 12, 5, 12, 0, 0))
19
+ end
20
+
21
+ it "should leave entries alone when the trim span is outside the entry range" do
22
+ @time_report.trim(Time.local(2009, 11, 1, 0, 0, 0), Time.local(2009, 12, 31, 0, 0, 0))
23
+ @time_report.entries[0].start_time.should eql(Time.local(2009, 12, 1, 9, 0, 0))
24
+ @time_report.entries[-1].end_time.should eql(Time.local(2009, 12, 5, 17, 0, 0))
25
+ end
26
+
27
+ it "should be able to produce a detail report" do
28
+ command_options = {:detail => true, :start => Time.local(2009, 12, 1), :end => Time.local(2009, 12, 6)}
29
+ stream = StringIO.new
30
+ @time_report.report(command_options, stream)
31
+ stream.string.should eql( <<EOS
32
+ +----------------------------------------------------------------------------+
33
+ | Id | Project | Start - Stop | Hours | Comment |
34
+ +----------------------------------------------------------------------------+
35
+ | 0 | ProjectB | 12/01/2009 at 01:00 AM to 05:00 PM | 16h 0m | comment 2 |
36
+ | 0 | ProjectA | 12/01/2009 at 09:00 AM to 12:00 PM | 3h 0m | comment 1 |
37
+ | 0 | ProjectA | 12/03/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 3 |
38
+ | 0 | ProjectB | 12/04/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 4 |
39
+ | 0 | ProjectC | 12/05/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 5 |
40
+ +----------------------------------------------------------------------------+
41
+ EOS
42
+ )
43
+ end
44
+
45
+ it "should be able to produce a detail report" do
46
+ command_options = {:detail => true, :start => Time.local(2009, 11, 1), :end => Time.local(2009, 12, 31)}
47
+ stream = StringIO.new
48
+ @time_report.report(command_options, stream)
49
+ stream.string.should eql( <<EOS
50
+ +----------------------------------------------------------------------------+
51
+ | Id | Project | Start - Stop | Hours | Comment |
52
+ +----------------------------------------------------------------------------+
53
+ | 0 | ProjectB | 12/01/2009 at 01:00 AM to 05:00 PM | 16h 0m | comment 2 |
54
+ | 0 | ProjectA | 12/01/2009 at 09:00 AM to 12:00 PM | 3h 0m | comment 1 |
55
+ | 0 | ProjectA | 12/03/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 3 |
56
+ | 0 | ProjectB | 12/04/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 4 |
57
+ | 0 | ProjectC | 12/05/2009 at 09:00 AM to 05:00 PM | 8h 0m | comment 5 |
58
+ +----------------------------------------------------------------------------+
59
+ EOS
60
+ )
61
+ end
62
+
63
+ it "should be able to produce a summary report" do
64
+ command_options = {:summary => true, :start => Time.local(2009, 12, 1), :end => Time.local(2009, 12, 6)}
65
+ stream = StringIO.new
66
+ @time_report.report(command_options, stream)
67
+ stream.string.should eql( <<EOS
68
+ ProjectA 11h 0m
69
+ ProjectB 24h 0m
70
+ ProjectC 8h 0m
71
+ -----------------
72
+ Total 43h 0m
73
+ EOS
74
+ )
75
+ end
76
+
77
+ it "should be able to produce a byday report" do
78
+ @entries << TimeEntry.new("ProjectB", Time.local(2009, 12, 1, 17, 0, 0), Time.local(2009, 12, 1, 19, 0, 0), "another comment")
79
+
80
+ command_options = {:byday => true, :start => Time.local(2009, 12, 1), :end => Time.local(2009, 12, 6)}
81
+ stream = StringIO.new
82
+ @time_report.report(command_options, stream)
83
+ stream.string.should eql( <<EOS
84
+ 12/01/09
85
+ ProjectA 3h 0m
86
+ - comment 1
87
+ ProjectB 18h 0m
88
+ - comment 2
89
+ - another comment
90
+ 12/03/09
91
+ ProjectA 8h 0m
92
+ - comment 3
93
+ 12/04/09
94
+ ProjectB 8h 0m
95
+ - comment 4
96
+ 12/05/09
97
+ ProjectC 8h 0m
98
+ - comment 5
99
+ EOS
100
+ )
101
+
102
+ end
103
+
104
+ end