samg-timetrap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README +148 -0
  2. data/Rakefile +16 -0
  3. data/bin/t +3 -0
  4. data/lib/timetrap.rb +317 -0
  5. data/spec/timetrap_spec.rb +373 -0
  6. metadata +107 -0
data/README ADDED
@@ -0,0 +1,148 @@
1
+ Timetrap
2
+ ========
3
+
4
+ Timetrap is a ruby port of Trevor Caira's Timebook, a small utility which aims
5
+ to be a low-overhead way of tracking what you spend time on. Timetrap
6
+ maintains its state in a sqlite3 database.
7
+
8
+ To install:
9
+ $ gem sources -a http://gems.github.com (you only have to do this once)
10
+ $ sudo gem install samg-timetrap
11
+
12
+ This will place a ``t`` executable in your path.
13
+
14
+ Original Timebook available at:
15
+ http://bitbucket.org/trevor/timebook/src/
16
+
17
+
18
+ Concepts
19
+ ~~~~~~~~
20
+
21
+ Timetrap maintains a list of *timesheets* -- distinct lists of timed *periods*.
22
+ Each period has a start and end time, with the exception of the most recent
23
+ period, which may have no end time set. This indicates that this period is
24
+ still running. Timesheets containing such periods are considered *active*. It
25
+ is possible to have multiple timesheets active simultaneously, though a single
26
+ time sheet may only have one period running at once.
27
+
28
+ Interactions with timetrap are performed through the ``t`` command on the
29
+ command line. ``t`` is followed by one of timetrap's subcommands. Often used
30
+ subcommands include ``in``, ``out``, ``switch``, ``now``, ``list`` and
31
+ ``display``. Commands may be abbreviated as long as they are unambiguous: thus
32
+ ``t switch foo`` and ``t s foo`` are identical. With the default command set,
33
+ no two commands share the first same letter, thus it is only necessary to type
34
+ the first letter of a command. Likewise, commands which display timesheets
35
+ accept abbreviated timesheet names. ``t display f`` is thus equivalent to ``t
36
+ display foo`` if ``foo`` is the only timesheet which begins with "f". Note that
37
+ this does not apply to ``t switch``, since this command also creates
38
+ timesheets. (Using the earlier example, if ``t switch f`` is entered, it would
39
+ thus be ambiguous whether a new timesheet ``f`` or switching to the existing
40
+ timesheet ``foo`` was desired).
41
+
42
+ Usage
43
+ ~~~~~
44
+
45
+ The basic usage is as follows::
46
+
47
+ $ t switch writing
48
+ $ t in document timetrap --at "10 minutes ago"
49
+ $ t out
50
+
51
+ The first command, ``t switch writing``, switches to the timesheet "writing"
52
+ (or creates it if it does not exist). ``t in document timetrap --at "10 minutes
53
+ ago"`` creates a new period in the current timesheet, and annotates it with the
54
+ description "document timetrap". The optional --at flag can be passed to start
55
+ the entry at a time other than the present. Any Chronic or database parsable
56
+ strings are accepted Note that this command would be in error if the
57
+ ``writing`` timesheet was already active. Finally, ``t out`` records the
58
+ current time as the end time for the most recent period in the ``writing``
59
+ timesheet.
60
+
61
+ To display the current timesheet, invoke the ``t display`` command::
62
+
63
+ $ t display
64
+ Timesheet writing:
65
+ Day Start End Duration Notes
66
+ Mar 14, 2009 19:53:30 - 20:06:15 0:12:45 document timetrap
67
+ 20:07:02 - 0:00:01 write home about timetrap
68
+ 0:12:46
69
+ Total 0:12:46
70
+
71
+ Each period in the timesheet is listed on a row. If the timesheet is active,
72
+ the final period in the timesheet will have no end time. After each day, the
73
+ total time tracked in the timesheet for that day is listed. Note that this is
74
+ computed by summing the durations of the periods beginning in the day. In the
75
+ last row, the total time tracked in the timesheet is shown.
76
+
77
+ Commands
78
+ ~~~~~~~~
79
+
80
+ **alter**
81
+ Inserts a note associated with the currently active period in the timesheet.
82
+
83
+ usage: ``t alter NOTES...``
84
+
85
+ **backend**
86
+ Run an interactive database session on the timetrap database. Requires the
87
+ sqlite3 command.
88
+
89
+ usage: ``t backend``
90
+
91
+ **display**
92
+ Display a given timesheet. If no timesheet is specified, show the current
93
+ timesheet.
94
+
95
+ usage: ``t display [TIMESHEET]``
96
+
97
+ **format**
98
+ Export the current sheet as a comma-separated value format spreadsheet. If
99
+ the final entry is active, it is ignored.
100
+
101
+ If a specific timesheet is given, display the same information for that
102
+ timesheet instead.
103
+
104
+ usage: ``t format [--start DATE] [--end DATE] [TIMESHEET]``
105
+
106
+ **in**
107
+ Start the timer for the current timesheet. Must be called before out. Notes
108
+ may be specified for this period. This is exactly equivalent to
109
+ ``t in; t alter NOTES``
110
+
111
+ usage: ``t in [--at TIME] [NOTES...]``
112
+
113
+ **kill**
114
+ Delete a timesheet. If no timesheet is specified, delete the current
115
+ timesheet and switch to the default timesheet.
116
+
117
+ usage: ``t kill [TIMESHEET]``
118
+
119
+ **list**
120
+ List the available timesheets.
121
+
122
+ usage: ``t list``
123
+
124
+ **now**
125
+ Print the current sheet, whether it's active, and if so, how long it has been
126
+ active and what notes are associated with the current period.
127
+
128
+ If a specific timesheet is given, display the same information for that
129
+ timesheet instead.
130
+
131
+ usage: ``t now``
132
+
133
+ **out**
134
+ Stop the timer for the current timesheet. Must be called after in.
135
+
136
+ usage: ``t in [--at TIME]``
137
+
138
+ **running**
139
+ Print all active sheets and any messages associated with them.
140
+
141
+ usage: ``t running``
142
+
143
+ **switch**
144
+ Switch to a new timesheet. this causes all future operation (except switch)
145
+ to operate on that timesheet. The default timesheet is called "default".
146
+
147
+ usage: ``t switch TIMESHEET``
148
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+
5
+ task :default => :spec
6
+
7
+ desc "Run all specs in spec directory"
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ Rake::RDocTask.new do |rd|
13
+ rd.main = "README"
14
+ rd.rdoc_dir = 'doc'
15
+ rd.rdoc_files.include("README", "**/*.rb")
16
+ end
data/bin/t ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'timetrap'
3
+ Timetrap::CLI.invoke
data/lib/timetrap.rb ADDED
@@ -0,0 +1,317 @@
1
+ require 'rubygems'
2
+ require 'chronic'
3
+ require 'sequel'
4
+ require 'Getopt/Declare'
5
+ # connect to database. This will create one if it doesn't exist
6
+ DB_NAME = defined?(TEST_MODE) ? nil : "#{ENV['HOME']}/.timetrap.db"
7
+ DB = Sequel.sqlite DB_NAME
8
+
9
+ module Timetrap
10
+ extend self
11
+
12
+ module CLI
13
+ attr_accessor :args
14
+ extend self
15
+
16
+ COMMANDS = {
17
+ "alter" => "alter the description of the active period",
18
+ "backend" => "open an the backend's interactive shell",
19
+ "display" => "display the current timesheet",
20
+ "format" => "export a sheet to csv format",
21
+ "in" => "start the timer for the current timesheet",
22
+ "kill" => "delete a timesheet",
23
+ "list" => "show the available timesheets",
24
+ "now" => "show the status of the current timesheet",
25
+ "out" => "stop the timer for the current timesheet",
26
+ "running" => "show all running timesheets",
27
+ "switch" => "switch to a new timesheet"
28
+ }
29
+
30
+ def parse arguments
31
+ args.parse arguments
32
+ end
33
+
34
+ def invoke
35
+ invoke_command_if_valid
36
+ end
37
+
38
+ def invoke_command_if_valid
39
+ command = args.unused.shift
40
+ case (valid = COMMANDS.keys.select{|name| name =~ %r|^#{command}|}).size
41
+ when 0 then say "Invalid command: #{command}"
42
+ when 1 then send valid[0]
43
+ else; say "Ambigous command: #{command}"; end
44
+ end
45
+
46
+ def alter
47
+ Timetrap.active_entry.update :note => args.unused.join(' ')
48
+ end
49
+
50
+ def backend
51
+ exec "sqlite3 #{DB_NAME}"
52
+ end
53
+
54
+ def in
55
+ Timetrap.start args.unused.join(' '), args['--at']
56
+ end
57
+
58
+ def out
59
+ Timetrap.stop args['--at']
60
+ end
61
+
62
+ def kill
63
+ sheet = args.unused.join(' ')
64
+ unless (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet)
65
+ say "ain't no sheet #{sheet.inspect}", 'sheets:', *sheets
66
+ return
67
+ end
68
+ victims = Entry.filter(:sheet => sheet).count
69
+ print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
70
+ if $stdin.gets =~ /\Aye?s?\Z/i
71
+ Timetrap.kill sheet
72
+ say "killed #{victims} entries"
73
+ else
74
+ say "will not kill"
75
+ end
76
+ end
77
+
78
+ def display
79
+ sheet = sheet_name_from_string(args.unused.join(' '))
80
+ sheet = (sheet =~ /.+/ ? sheet : Timetrap.current_sheet)
81
+ say "Timesheet: #{sheet}"
82
+ say " Day Start End Duration Notes"
83
+ last_start = nil
84
+ from_current_day = []
85
+ (ee = Timetrap.entries(sheet)).each_with_index do |e, i|
86
+
87
+
88
+ from_current_day << e
89
+ e_end = e.end || Time.now
90
+ say "%27s%11s -%9s%10s %s" % [
91
+ format_date_if_new(e.start, last_start),
92
+ format_time(e.start),
93
+ format_time(e.end),
94
+ format_duration(e.start, e_end),
95
+ e.note
96
+ ]
97
+
98
+ nxt = Timetrap.entries(sheet).map[i+1]
99
+ if nxt == nil or !same_day?(e.start, nxt.start)
100
+ say "%59s" % format_total(from_current_day)
101
+ from_current_day = []
102
+ else
103
+ end
104
+ last_start = e.start
105
+ end
106
+ say <<-OUT
107
+ ---------------------------------------------------------
108
+ OUT
109
+ say " Total%43s" % format_total(ee)
110
+ end
111
+
112
+ def format
113
+ say "Sorry not implemented yet :-("
114
+ end
115
+
116
+ def switch
117
+ sheet = args.unused.join(' ')
118
+ if not sheet then say "No sheet specified"; return end
119
+ say "Switching to sheet " + Timetrap.switch(sheet)
120
+ end
121
+
122
+ def list
123
+ sheets = Entry.map{|e|e.sheet}.uniq.sort.map do |sheet|
124
+ sheet_atts = {:total => 0, :running => 0, :today => 0}
125
+ DB[:entries].filter(:sheet => sheet).inject(sheet_atts) do |m, e|
126
+ e_end = e[:end] || Time.now
127
+ m[:name] ||= sheet
128
+ m[:total] += (e_end.to_i - e[:start].to_i)
129
+ m[:running] += (e_end.to_i - e[:start].to_i) unless e[:end]
130
+ m[:today] += (e_end.to_i - e[:start].to_i) if same_day?(Time.now, e[:start])
131
+ m
132
+ end
133
+ end
134
+ width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
135
+ say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
136
+ sheets.each do |sheet|
137
+ star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
138
+ say "#{star}%-#{width}s%-12s%-12s%s" % [
139
+ sheet[:running],
140
+ sheet[:today],
141
+ sheet[:total]
142
+ ].map(&method(:format_seconds)).unshift(sheet[:name])
143
+ end
144
+ end
145
+
146
+ def now
147
+ if Timetrap.running?
148
+ out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Time.now)}".gsub(/ /, ' ')
149
+ out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
150
+ say out
151
+ else
152
+ say "#{Timetrap.current_sheet}: not running"
153
+ end
154
+ end
155
+
156
+
157
+ private
158
+
159
+ def format_time time
160
+ return '' unless time.respond_to?(:strftime)
161
+ time.strftime('%H:%M:%S')
162
+ end
163
+
164
+ def format_date time
165
+ return '' unless time.respond_to?(:strftime)
166
+ time.strftime('%a %b %d, %Y')
167
+ end
168
+
169
+ def format_date_if_new time, last_time
170
+ return '' unless time.respond_to?(:strftime)
171
+ same_day?(time, last_time) ? '' : format_date(time)
172
+ end
173
+
174
+ def same_day? time, other_time
175
+ format_date(time) == format_date(other_time)
176
+ end
177
+
178
+ def format_duration stime, etime
179
+ return '' unless stime and etime
180
+ secs = etime.to_i - stime.to_i
181
+ format_seconds secs
182
+ end
183
+
184
+ def format_seconds secs
185
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
186
+ end
187
+
188
+ def format_total entries
189
+ secs = entries.inject(0){|m, e|e_end = e.end || Time.now; m += e_end.to_i - e.start.to_i if e_end && e.start;m}
190
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
191
+ end
192
+
193
+ def sheet_name_from_string string
194
+ return "" unless string =~ /.+/
195
+ DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
196
+ rescue
197
+ ""
198
+ end
199
+
200
+ public
201
+ def say *something
202
+ puts *something
203
+ end
204
+ end
205
+
206
+ def current_sheet= sheet
207
+ m = Meta.find_or_create(:key => 'current_sheet')
208
+ m.value = sheet
209
+ m.save
210
+ end
211
+
212
+ def current_sheet
213
+ unless Meta.find(:key => 'current_sheet')
214
+ Meta.create(:key => 'current_sheet', :value => 'default')
215
+ end
216
+ Meta.find(:key => 'current_sheet').value
217
+ end
218
+
219
+ def invoked_as_executable?
220
+ $0 == __FILE__
221
+ end
222
+
223
+ def entries sheet = nil
224
+ Entry.filter(:sheet => sheet).order_by(:start)
225
+ end
226
+
227
+ def running?
228
+ !!active_entry
229
+ end
230
+
231
+ def active_entry
232
+ Entry.find(:sheet => Timetrap.current_sheet, :end => nil)
233
+ end
234
+
235
+ def stop time = nil
236
+ while a = active_entry
237
+ time ||= Time.now
238
+ a.end = time
239
+ a.save
240
+ end
241
+ end
242
+
243
+ def start note, time = nil
244
+ raise AlreadyRunning if running?
245
+ time ||= Time.now
246
+ Entry.create(:sheet => Timetrap.current_sheet, :note => note, :start => time).save
247
+ rescue => e
248
+ CLI.say e.message
249
+ end
250
+
251
+ def switch sheet
252
+ self.current_sheet = sheet
253
+ end
254
+
255
+ def kill sheet
256
+ Entry.filter(:sheet => sheet).destroy
257
+ end
258
+
259
+ class AlreadyRunning < StandardError
260
+ def message
261
+ "Timetrap is already running"
262
+ end
263
+ end
264
+
265
+
266
+ class Entry < Sequel::Model
267
+ plugin :schema
268
+
269
+ def start= time
270
+ self[:start]= Chronic.parse(time) || time
271
+ end
272
+
273
+ def end= time
274
+ self[:end]= Chronic.parse(time) || time
275
+ end
276
+
277
+ # do a quick pseudo migration. This should only get executed on the first run
278
+ set_schema do
279
+ primary_key :id
280
+ column :note, :string
281
+ column :start, :timestamp
282
+ column :end, :timestamp
283
+ column :sheet, :string
284
+ end
285
+ create_table unless table_exists?
286
+ end
287
+
288
+ class Meta < Sequel::Model(:meta)
289
+ plugin :schema
290
+
291
+ set_schema do
292
+ primary_key :id
293
+ column :key, :string
294
+ column :value, :string
295
+ end
296
+ create_table unless table_exists?
297
+ end
298
+ CLI.args = Getopt::Declare.new(<<-EOF)
299
+ Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
300
+
301
+ where COMMAND is one of:
302
+ alter - alter the description of the active period
303
+ backend - open an the backend's interactive shell
304
+ display - display the current timesheet
305
+ format - export a sheet to csv format
306
+ in - start the timer for the current timesheet
307
+ kill - delete a timesheet
308
+ list - show the available timesheets
309
+ now - show the status of the current timesheet
310
+ out - stop the timer for the current timesheet
311
+ running - show all running timesheets
312
+ switch - switch to a new timesheet
313
+
314
+ COMMAND OPTIONS
315
+ -a, --at <time:qs> Use this time instead of now
316
+ EOF
317
+ end
@@ -0,0 +1,373 @@
1
+ TEST_MODE = true
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'timetrap')
3
+ require 'spec'
4
+
5
+ describe Timetrap do
6
+ def create_entry atts = {}
7
+ Timetrap::Entry.create({
8
+ :sheet => 's1',
9
+ :start => Time.now,
10
+ :end => Time.now,
11
+ :note => 'note'}.merge(atts))
12
+ end
13
+
14
+ before :each do
15
+ Timetrap::Entry.create_table!
16
+ Timetrap::Meta.create_table!
17
+ $stdout = StringIO.new
18
+ $stdin = StringIO.new
19
+ end
20
+
21
+ describe 'CLI' do
22
+ describe "COMMANDS" do
23
+ def invoke command
24
+ Timetrap::CLI.parse command
25
+ Timetrap::CLI.invoke
26
+ end
27
+
28
+ describe 'alter' do
29
+ before do
30
+ Timetrap.start "running entry", nil
31
+ end
32
+ it "should alter the description of the active period" do
33
+ Timetrap.active_entry.note.should == 'running entry'
34
+ invoke 'alter new description'
35
+ Timetrap.active_entry.note.should == 'new description'
36
+ end
37
+ end
38
+
39
+ describe "backend" do
40
+ it "should open an sqlite console to the db" do
41
+ Timetrap::CLI.should_receive(:exec).with("sqlite3 #{DB_NAME}")
42
+ invoke 'backend'
43
+ end
44
+ end
45
+
46
+ describe "display" do
47
+ before do
48
+ Timetrap::Entry.create( :sheet => 'another',
49
+ :note => 'entry 4', :start => '2008-10-05 18:00:00'
50
+ )
51
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
52
+ :note => 'entry 2', :start => '2008-10-03 16:00:00', :end => '2008-10-03 18:00:00'
53
+ )
54
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
55
+ :note => 'entry 1', :start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00'
56
+ )
57
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
58
+ :note => 'entry 3', :start => '2008-10-05 16:00:00', :end => '2008-10-05 18:00:00'
59
+ )
60
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
61
+ :note => 'entry 4', :start => '2008-10-05 18:00:00'
62
+ )
63
+
64
+ Time.stub!(:now).and_return Time.at(1223254800 + (60*60*2))
65
+ @desired_output = <<-OUTPUT
66
+ Timesheet: SpecSheet
67
+ Day Start End Duration Notes
68
+ Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
69
+ 16:00:00 - 18:00:00 2:00:00 entry 2
70
+ 4:00:00
71
+ Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
72
+ 18:00:00 - 2:00:00 entry 4
73
+ 4:00:00
74
+ ---------------------------------------------------------
75
+ Total 8:00:00
76
+ OUTPUT
77
+ end
78
+
79
+ it "should display the current timesheet" do
80
+ Timetrap.current_sheet = 'SpecSheet'
81
+ invoke 'display'
82
+ $stdout.string.should == @desired_output
83
+ end
84
+
85
+ it "should display a non current timesheet" do
86
+ Timetrap.current_sheet = 'another'
87
+ invoke 'display SpecSheet'
88
+ $stdout.string.should == @desired_output
89
+ end
90
+
91
+ it "should display a non current timesheet based on a partial name match" do
92
+ Timetrap.current_sheet = 'another'
93
+ invoke 'display S'
94
+ $stdout.string.should == @desired_output
95
+ end
96
+ end
97
+
98
+ describe "format" do
99
+ it "should export a sheet to a csv format" do
100
+ pending
101
+ end
102
+ end
103
+
104
+ describe "in" do
105
+ it "should start the time for the current timesheet" do
106
+ lambda do
107
+ invoke 'in'
108
+ end.should change(Timetrap::Entry, :count).by(1)
109
+ end
110
+
111
+ it "should set the note when starting a new entry" do
112
+ invoke 'in working on something'
113
+ Timetrap::Entry.order_by(:id).last.note.should == 'working on something'
114
+ end
115
+
116
+ it "should set the start when starting a new entry" do
117
+ @time = Time.now
118
+ Time.stub!(:now).and_return @time
119
+ invoke 'in working on something'
120
+ Timetrap::Entry.order_by(:id).last.start.to_i.should == @time.to_i
121
+ end
122
+
123
+ it "should not start the time if the timetrap is running" do
124
+ Timetrap.stub!(:running?).and_return true
125
+ lambda do
126
+ invoke 'in'
127
+ end.should_not change(Timetrap::Entry, :count)
128
+ end
129
+
130
+ it "should allow the sheet to be started at a certain time" do
131
+ invoke 'in work --at "10am 2008-10-03"'
132
+ Timetrap::Entry.order_by(:id).last.start.should == Time.parse('2008-10-03 10:00')
133
+ end
134
+ end
135
+
136
+ describe "kill" do
137
+ it "should give me a chance not to fuck up" do
138
+ entry = create_entry
139
+ lambda do
140
+ $stdin.string = ""
141
+ invoke "kill #{entry.sheet}"
142
+ end.should_not change(Timetrap::Entry, :count).by(-1)
143
+ end
144
+
145
+ it "should delete a timesheet" do
146
+ entry = create_entry
147
+ lambda do
148
+ $stdin.string = "yes\n"
149
+ invoke "kill #{entry.sheet}"
150
+ end.should change(Timetrap::Entry, :count).by(-1)
151
+ end
152
+ end
153
+
154
+ describe "list" do
155
+ before do
156
+ Time.stub!(:now).and_return Time.parse("Oct 5 18:00:00 -0700 2008")
157
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-03 12:00:00',
158
+ :end => '2008-10-03 14:00:00')
159
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-03 12:00:00',
160
+ :end => '2008-10-03 14:00:00')
161
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-05 12:00:00',
162
+ :end => '2008-10-05 14:00:00')
163
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-05 14:00:00',
164
+ :end => nil)
165
+ create_entry( :sheet => 'Sheet 1', :start => '2008-10-03 16:00:00',
166
+ :end => '2008-10-03 18:00:00')
167
+ Timetrap.current_sheet = 'A Longly Named Sheet 2'
168
+ end
169
+ it "should list available timesheets" do
170
+ invoke 'list'
171
+ $stdout.string.should == <<-OUTPUT
172
+ Timesheet Running Today Total Time
173
+ *A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00
174
+ Sheet 1 0:00:00 0:00:00 2:00:00
175
+ OUTPUT
176
+ end
177
+ end
178
+
179
+ describe "now" do
180
+ before do
181
+ Timetrap.current_sheet = 'current sheet'
182
+ end
183
+
184
+ describe "when the current timesheet isn't running" do
185
+ it "should show that it isn't running" do
186
+ invoke 'now'
187
+ $stdout.string.should == <<-OUTPUT
188
+ current sheet: not running
189
+ OUTPUT
190
+ end
191
+ end
192
+
193
+ describe "when the current timesheet is running" do
194
+ before do
195
+ invoke 'in a timesheet that is running'
196
+ @entry = Timetrap.active_entry
197
+ @entry.stub!(:start).and_return(Time.at(0))
198
+ Time.stub!(:now).and_return Time.at(60)
199
+ Timetrap.stub!(:active_entry).and_return @entry
200
+ end
201
+
202
+ it "should show how long the current item is running for" do
203
+ invoke 'now'
204
+ $stdout.string.should == <<-OUTPUT
205
+ current sheet: 0:01:00 (a timesheet that is running)
206
+ OUTPUT
207
+ end
208
+ end
209
+ end
210
+
211
+ describe "out" do
212
+ before :each do
213
+ invoke 'in'
214
+ @active = Timetrap.active_entry
215
+ @now = Time.now
216
+ Time.stub!(:now).and_return @now
217
+ end
218
+ it "should set the stop for the running entry" do
219
+ @active.refresh.end.should == nil
220
+ invoke 'out'
221
+ @active.refresh.end.to_i.should == @now.to_i
222
+ end
223
+
224
+ it "should not do anything if nothing is running" do
225
+ lambda do
226
+ invoke 'out'
227
+ invoke 'out'
228
+ end.should_not raise_error
229
+ end
230
+
231
+ it "should allow the sheet to be stopped at a certain time" do
232
+ invoke 'out --at "10am 2008-10-03"'
233
+ Timetrap::Entry.order_by(:id).last.end.should == Time.parse('2008-10-03 10:00')
234
+ end
235
+ end
236
+
237
+ describe "running" do
238
+ before do
239
+ create_entry :sheet => 'one', :end => nil
240
+ create_entry :sheet => 'two', :end => nil
241
+ end
242
+ it "should show all running timesheets" do
243
+
244
+
245
+ end
246
+ end
247
+
248
+ describe "switch" do
249
+ it "should switch to a new timesheet" do
250
+ invoke 'switch sheet 1'
251
+ Timetrap.current_sheet.should == 'sheet 1'
252
+ invoke 'switch sheet 2'
253
+ Timetrap.current_sheet.should == 'sheet 2'
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "entries" do
260
+ it "should give the entires for a sheet" do
261
+ e = create_entry :sheet => 'sheet'
262
+ Timetrap.entries('sheet').all.should include(e)
263
+ end
264
+
265
+ end
266
+
267
+ describe "start" do
268
+ it "should start an new entry" do
269
+ @time = Time.now
270
+ Timetrap.current_sheet = 'sheet1'
271
+ lambda do
272
+ Timetrap.start 'some work', @time
273
+ end.should change(Timetrap::Entry, :count).by(1)
274
+ Timetrap::Entry.order(:id).last.sheet.should == 'sheet1'
275
+ Timetrap::Entry.order(:id).last.note.should == 'some work'
276
+ Timetrap::Entry.order(:id).last.start.to_i.should == @time.to_i
277
+ Timetrap::Entry.order(:id).last.end.should be_nil
278
+ end
279
+
280
+ it "should be running if it is started" do
281
+ Timetrap.should_not be_running
282
+ Timetrap.start 'some work', @time
283
+ Timetrap.should be_running
284
+ end
285
+
286
+ it "should raise and error if it is already running" do
287
+ lambda do
288
+ Timetrap.start 'some work', @time
289
+ Timetrap.start 'some work', @time
290
+ end.should change(Timetrap::Entry, :count).by(1)
291
+ end
292
+ end
293
+
294
+ describe "stop" do
295
+ it "should stop a new entry" do
296
+ @time = Time.now
297
+ Timetrap.start 'some work', @time
298
+ entry = Timetrap.active_entry
299
+ entry.end.should be_nil
300
+ Timetrap.stop @time
301
+ entry.refresh.end.to_i.should == @time.to_i
302
+ end
303
+
304
+ it "should not be running if it is stopped" do
305
+ Timetrap.should_not be_running
306
+ Timetrap.start 'some work', @time
307
+ Timetrap.stop
308
+ Timetrap.should_not be_running
309
+ end
310
+
311
+ it "should not stop it twice" do
312
+ Timetrap.start 'some work'
313
+ e = Timetrap.active_entry
314
+ Timetrap.stop
315
+ time = e.refresh.end
316
+ Timetrap.stop
317
+ time.to_i.should == e.refresh.end.to_i
318
+ end
319
+
320
+ end
321
+
322
+ describe 'switch' do
323
+ it "should switch to a new sheet" do
324
+ Timetrap.switch 'sheet1'
325
+ Timetrap.current_sheet.should == 'sheet1'
326
+ Timetrap.switch 'sheet2'
327
+ Timetrap.current_sheet.should == 'sheet2'
328
+ end
329
+ end
330
+ end
331
+
332
+ describe Timetrap::Entry do
333
+ before do
334
+ @time = Time.now
335
+ @entry = Timetrap::Entry.new
336
+ end
337
+
338
+ describe 'attributes' do
339
+ it "should have a note" do
340
+ @entry.note = "world takeover"
341
+ @entry.note.should == "world takeover"
342
+ end
343
+
344
+ it "should have a start" do
345
+ @entry.start = @time
346
+ @entry.start.should == @time
347
+ end
348
+
349
+ it "should have a end" do
350
+ @entry.end = @time
351
+ @entry.end.should == @time
352
+ end
353
+
354
+ it "should have a sheet" do
355
+ @entry.sheet= 'name'
356
+ @entry.sheet.should == 'name'
357
+ end
358
+ end
359
+
360
+ describe "parsing natural language times" do
361
+ it "should set start time using english" do
362
+ @entry.start = "yesterday 10am"
363
+ @entry.start.should_not be_nil
364
+ @entry.start.should == Chronic.parse("yesterday 10am")
365
+ end
366
+
367
+ it "should set end time using english" do
368
+ @entry.end = "tomorrow 1pm"
369
+ @entry.end.should_not be_nil
370
+ @entry.end.should == Chronic.parse("tomorrow 1pm")
371
+ end
372
+ end
373
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: samg-timetrap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sam Goldstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sequel
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.12.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: chronic
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.3
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: getopt-declare
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "1.28"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: mime-types
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "1.15"
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: diff-lcs
57
+ type: :runtime
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.1.2
64
+ version:
65
+ description: Command line time tracker
66
+ email: sgrock@gmail.com
67
+ executables:
68
+ - t
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - README
75
+ - Rakefile
76
+ - lib/timetrap.rb
77
+ - bin/t
78
+ - spec/timetrap_spec.rb
79
+ has_rdoc: true
80
+ homepage: http://github.com/samg/timetrap/tree/master
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --inline-source
84
+ - --charset=UTF-8
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ version:
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ version:
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.2.0
103
+ signing_key:
104
+ specification_version: 2
105
+ summary: Command line time tracker
106
+ test_files: []
107
+