did 0.2.01

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,52 @@
1
+ = DID - timetracking for the non-clairvoyant
2
+
3
+ Did version 0.2.0
4
+
5
+ This package contains Did, a command-line utility for quickly
6
+ recording what you last worked on. More than just a tool, Did is also
7
+ a mindset: it brings your awareness to task switches, those notorious
8
+ time sinks.
9
+
10
+ == Installation
11
+
12
+ === Gem installation
13
+
14
+ Download and install Did with the following
15
+
16
+ gem install did
17
+
18
+ For bash autocompletion of tags, add the following to the end of your
19
+ .bashrc file
20
+
21
+ complete -C `which did_autocomplete | tail -n 1` did
22
+
23
+ == Usage
24
+
25
+ Tell Did that you've started working.
26
+
27
+ did sit!
28
+
29
+ Then, when you find yourself changing tasks, tell Did what you were just doing.
30
+
31
+ did [tag 1] [tag 2] ...
32
+
33
+ After a while, you can ask what you did today.
34
+
35
+ did what?
36
+
37
+ Or ask how your time has broken down across your descriptions of tasks.
38
+
39
+ did what? tags
40
+
41
+
42
+ == Credits
43
+
44
+ Written by Andrew Paradise in 2011.
45
+
46
+
47
+ == Warranty
48
+
49
+ This software is provided "as is" and without any express or
50
+ implied warranties, including, without limitation, the implied
51
+ warranties of merchantibility and fitness for a particular
52
+ purpose.
data/bin/did ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ALLOW_CREATE = true
4
+
5
+ require 'did'
6
+
7
+ action = Did::Action.parse(ARGV)
8
+ action.perform
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ALLOW_CREATE = false
4
+
5
+ require 'did'
6
+
7
+ exit unless $env.database_file_exist?
8
+ command, partial, prior = ARGV[0..2]
9
+ tags = Did::Tag.find(:all, :conditions => ["label like ?", partial + "%"])
10
+ tags.each do |tag|
11
+ puts tag.label
12
+ end
data/db/config.yml ADDED
@@ -0,0 +1,5 @@
1
+
2
+
3
+ test:
4
+ adapter: sqlite3
5
+ database: db/test.sq3
@@ -0,0 +1,13 @@
1
+ class CreateTagTable < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.string :label
5
+ end
6
+ add_index :tags, [:label], :unique => true
7
+ end
8
+
9
+ def self.down
10
+ remove_index :tags, :label
11
+ drop_table :tags
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ class CreateWorkTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :spans do |t|
4
+ t.timestamp :start_time
5
+ t.timestamp :end_time
6
+ end
7
+
8
+ create_table :spans_tags, :id => false do |t|
9
+ t.integer :span_id
10
+ t.integer :tag_id
11
+ end
12
+ add_index :spans_tags, :tag_id
13
+ end
14
+
15
+ def self.down
16
+ remove_index :spans_tags, :tag_id
17
+ drop_table :spans_tags
18
+ drop_table :spans
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ class CreateSittingTable < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :sittings do |t|
4
+ t.timestamp :start_time
5
+ t.timestamp :end_time
6
+ end
7
+ add_column :spans, :sitting_id, :integer
8
+ end
9
+
10
+ def self.down
11
+ remove_column :spans, :sitting_id
12
+ drop_table :sittings
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ class AddCurrentFlagToSitting < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :sittings, :current, :boolean
4
+ add_index :sittings, [:current]
5
+ end
6
+
7
+ def self.down
8
+ remove_index :sittings, [:current]
9
+ remove_column :sittings, :current
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class AddSittingStartEndToSpan < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :spans, :sitting_start, :boolean
4
+ add_column :spans, :sitting_end, :boolean
5
+ add_column :sittings, :end_span_id, :integer
6
+ end
7
+
8
+ def self.down
9
+ remove_column :sittings, :end_span_id
10
+ remove_column :spans, :sitting_end
11
+ remove_column :spans, :sitting_start
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ class AddSittingTags < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :sittings, :tag_labels, :string
4
+ end
5
+
6
+ def self.down
7
+ remove_column :sittings, :tag_labels
8
+ end
9
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,45 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended to check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(:version => 20110521174213) do
14
+
15
+ create_table "sittings", :force => true do |t|
16
+ t.datetime "start_time"
17
+ t.datetime "end_time"
18
+ t.boolean "current"
19
+ t.integer "end_span_id"
20
+ end
21
+
22
+ add_index "sittings", ["current"], :name => "index_sittings_on_current"
23
+
24
+ create_table "spans", :force => true do |t|
25
+ t.datetime "start_time"
26
+ t.datetime "end_time"
27
+ t.integer "sitting_id"
28
+ t.boolean "sitting_start"
29
+ t.boolean "sitting_end"
30
+ end
31
+
32
+ create_table "spans_tags", :id => false, :force => true do |t|
33
+ t.integer "span_id"
34
+ t.integer "tag_id"
35
+ end
36
+
37
+ add_index "spans_tags", ["tag_id"], :name => "index_spans_tags_on_tag_id"
38
+
39
+ create_table "tags", :force => true do |t|
40
+ t.string "label"
41
+ end
42
+
43
+ add_index "tags", ["label"], :name => "index_tags_on_label", :unique => true
44
+
45
+ end
data/db/schema.sql ADDED
@@ -0,0 +1,9 @@
1
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
2
+ CREATE TABLE "sittings" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "start_time" datetime, "end_time" datetime, "current" boolean, "end_span_id" integer);
3
+ CREATE TABLE "spans" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "start_time" datetime, "end_time" datetime, "sitting_id" integer, "sitting_start" boolean, "sitting_end" boolean);
4
+ CREATE TABLE "spans_tags" ("span_id" integer, "tag_id" integer);
5
+ CREATE TABLE "tags" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "label" varchar(255));
6
+ CREATE INDEX "index_sittings_on_current" ON "sittings" ("current");
7
+ CREATE INDEX "index_spans_tags_on_tag_id" ON "spans_tags" ("tag_id");
8
+ CREATE UNIQUE INDEX "index_tags_on_label" ON "tags" ("label");
9
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
data/lib/did.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'did/env'
2
+ require 'did/action'
3
+
4
+ module Did
5
+ VERSION = "0.2.01"
6
+ end
data/lib/did/action.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'did/action/sit'
2
+ require 'did/action/submit'
3
+ require 'did/action/report'
4
+
5
+ module Did
6
+ module Action
7
+ def self.parse(argv)
8
+ if argv[0] == "sit!"
9
+ parse_sit(argv)
10
+ elsif argv[0] == "what?"
11
+ parse_report(argv)
12
+ else
13
+ parse_submit(argv)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def self.parse_sit(argv)
20
+ sit = Sit.new
21
+ sit.start_time = Time.now
22
+
23
+ sit
24
+ end
25
+
26
+ def self.parse_submit(argv)
27
+ submit = Submit.new
28
+ submit.labels = argv
29
+ submit.start_time = 2.hours.ago
30
+ submit.end_time = Time.now
31
+
32
+ submit
33
+ end
34
+
35
+ def self.parse_report(argv)
36
+ report = Report.new
37
+
38
+ report.format = (argv[1] || "timeline").to_sym
39
+ report.range = Date.today
40
+
41
+ report
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+
2
+
3
+ module Did
4
+ module Action
5
+ class Report
6
+ attr_accessor :format
7
+ attr_accessor :range
8
+
9
+ def initialize
10
+ end
11
+
12
+ def perform
13
+ case @format
14
+ when :timeline
15
+ perform_timeline
16
+ when :tags
17
+ perform_tags
18
+ end
19
+ end
20
+
21
+ def perform_timeline
22
+ timeline_report(Span.find_for_day(@range))
23
+ end
24
+
25
+ def perform_tags
26
+ tags_report(Span.find_for_day(@range))
27
+ end
28
+
29
+ def timeline_report(spans)
30
+ spans.each do |span|
31
+ sitting_char = "|"
32
+ if span.entire_sitting?
33
+ sitting_char = "]"
34
+ elsif span.sitting_start?
35
+ sitting_char = "\\"
36
+ elsif span.sitting_end?
37
+ sitting_char = "/"
38
+ end
39
+
40
+ puts "#{span.start_time} - #{span.end_time} (#{duration_to_s(span.duration)}): #{sitting_char} #{span.tags.map{|t| t.label}.join(", ")}"
41
+ end
42
+ end
43
+
44
+ def tags_report(spans)
45
+ tag_totals = Hash.new{|hash, key| hash[key] = 0}
46
+ spans.each do |span|
47
+ span.tags.each do |tag|
48
+ tag_totals[tag.label]+= span.duration
49
+ end
50
+ end
51
+ max_length = tag_totals.keys.map(&:length).max
52
+ tag_totals = tag_totals.sort{|a, b| a[1] <=> b[1]}.reverse
53
+ tag_totals.each do |tag_label, duration|
54
+ puts "#{tag_label.ljust(max_length + 3)} #{duration_to_s(duration)}"
55
+ end
56
+ end
57
+
58
+ def duration_to_s(duration)
59
+ total_seconds = duration.floor
60
+ milliseconds = duration - total_seconds
61
+ seconds = total_seconds % 60
62
+ minute_seconds = (total_seconds - seconds) % 3600
63
+ hour_seconds = total_seconds - seconds - minute_seconds
64
+
65
+ "%03d:%02d:%02d" % [hour_seconds / 3600, minute_seconds / 60, seconds]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,20 @@
1
+ require 'did/sitting'
2
+
3
+ module Did
4
+ module Action
5
+ class Sit
6
+ attr_accessor :start_time
7
+
8
+ def perform
9
+ current_sitting = Sitting.find(:first, :conditions => {:current => true})
10
+
11
+ if current_sitting && current_sitting.end_time.nil?
12
+ current_sitting.update_attributes(:start_time => @start_time)
13
+ else
14
+ current_sitting.update_attributes(:current => false) if current_sitting
15
+ Sitting.create(:start_time => @start_time, :current => true)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ require 'did/tag'
2
+ require 'did/span'
3
+ require 'did/sitting'
4
+
5
+ module Did
6
+ module Action
7
+ class Submit
8
+ attr_accessor :labels
9
+ attr_accessor :start_time
10
+ attr_accessor :end_time
11
+
12
+ def initialize()
13
+ @resolved = false
14
+ end
15
+
16
+ def perform
17
+ tags = labels.map do |label|
18
+ Tag.find_or_create_by_label(label);
19
+ end
20
+
21
+ prior_sitting = Sitting.find_current
22
+ current_sitting = nil
23
+ sitting_start = true
24
+ if prior_sitting && prior_sitting.end_time.nil?
25
+ if prior_sitting.start_time > @start_time
26
+ @start_time = prior_sitting.start_time
27
+ end
28
+ current_sitting = prior_sitting
29
+ prior_sitting = nil
30
+ elsif prior_sitting && prior_sitting.end_time > @start_time
31
+ @start_time = prior_sitting.end_time + 1.second
32
+ current_sitting = prior_sitting
33
+ sitting_start = false
34
+ end
35
+
36
+ prior_span = prior_sitting.end_span if prior_sitting
37
+ if prior_span && prior_span.tags == tags
38
+ prior_span.update_attributes(:end_time => @end_time)
39
+ prior_sitting.update_attributes(:end_time => @end_time)
40
+ return
41
+ end
42
+
43
+ span = Span.create(:tags => tags,
44
+ :end_time => @end_time, :start_time => @start_time,
45
+ :sitting_start => sitting_start, :sitting_end => true)
46
+
47
+ # We are about to start a new sitting, so we close out the old.
48
+ if prior_sitting && current_sitting.nil?
49
+ prior_sitting.update_attributes(:current => false)
50
+ end
51
+
52
+ if current_sitting
53
+ # Shift end time of sitting to include new span.
54
+ current_sitting.end_span.update_attributes(:sitting_end => false) if current_sitting.end_span
55
+ params = {
56
+ :end_time => @end_time,
57
+ :end_span_id => span.id
58
+ }
59
+
60
+ # We are commandeering the last sitting created by a 'sit' action.
61
+ params[:start_time] = @start_time if current_sitting.end_time.nil?
62
+ current_sitting.update_attributes(params)
63
+ end
64
+
65
+ unless current_sitting
66
+ current_sitting = Sitting.create(:start_time => @start_time, :end_time => @end_time,
67
+ :end_span_id => span.id, :current => true)
68
+ end
69
+
70
+ span.update_attributes(:sitting_id => current_sitting.id)
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/did/env.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'active_record'
2
+ require 'pathname'
3
+ require 'sqlite3'
4
+
5
+ class Env
6
+ attr_reader :profile_name
7
+
8
+ def initialize(profile_name, allow_create = true)
9
+ @profile_name = profile_name
10
+ @allow_create = allow_create
11
+ end
12
+
13
+ def allow_create?
14
+ @allow_create
15
+ end
16
+
17
+ def setup_active_record
18
+ initialize_home
19
+ config = {"adapter" => 'sqlite3', "database" => database_path.to_s}
20
+ ActiveRecord::Base.establish_connection(config)
21
+ end
22
+
23
+ def setup_development(environment_name)
24
+ config = development_database_config(environment_name)
25
+ ActiveRecord::Base.establish_connection(config)
26
+ end
27
+
28
+ def database_file_exist?
29
+ database_path.exist?
30
+ end
31
+
32
+ def db
33
+ return nil if !allow_create? && !database_file_exist?
34
+ @@db ||= SQLite3::Database.new(database_path.to_s)
35
+ end
36
+
37
+ private
38
+
39
+ def initialize_home
40
+ did_home.mkpath
41
+ database_path.dirname.mkpath
42
+ if !database_file_exist?
43
+ puts "initializing DID database: #{database_path}"
44
+ File.readlines("db/schema.sql").each do |line|
45
+ db.execute(line)
46
+ end
47
+ end
48
+ end
49
+
50
+ def did_home
51
+ Pathname.new('~').expand_path + ".did"
52
+ end
53
+
54
+ def database_path
55
+ did_home + profile_name + "did.db"
56
+ end
57
+
58
+ def development_database_config(environment_name)
59
+ home = File.dirname(__FILE__) + '/../../'
60
+ database_config_path = home + 'db/config.yml'
61
+ raise "database configuration file not found at: #{database_config_path}" unless File.exist?(database_config_path)
62
+ config_all = YAML.load(File.read(database_config_path))
63
+ config = config_all[environment_name]
64
+ raise "#{environment_name} environment not configured in #{database_config_path}" unless config
65
+ if config['adapter'] == 'sqlite3' && config['database'][0] != "/"
66
+ config['database'] = (home + config['database'])
67
+ end
68
+
69
+ config
70
+ end
71
+ end
72
+
73
+ DID_PROFILE = ENV['DID_PROFILE'] || 'default'
74
+ $env = Env.new(DID_PROFILE, defined?(ALLOW_CREATE) ? ALLOW_CREATE : true)
75
+ exit if !$env.allow_create? && !$env.database_file_exist?
76
+ unless defined?(ENVIRONMENT_NAME)
77
+ $env.setup_active_record
78
+ else
79
+ $env.setup_development(ENVIRONMENT_NAME)
80
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module Did
3
+ class Sitting < ActiveRecord::Base
4
+ belongs_to :end_span, :class_name => "Span"
5
+
6
+ def self.find_current
7
+ find(:first, :conditions => {:current => true})
8
+ end
9
+ end
10
+ end
data/lib/did/span.rb ADDED
@@ -0,0 +1,24 @@
1
+
2
+
3
+ module Did
4
+ class Span < ActiveRecord::Base
5
+ has_and_belongs_to_many :tags
6
+ belongs_to :sitting
7
+
8
+ def entire_sitting?
9
+ sitting_start? && sitting_end?
10
+ end
11
+
12
+ def self.find_for_day(date)
13
+ Span.find(:all, :conditions => ["end_time >= ? AND start_time <= ?", date, date + 1.day], :order => :start_time)
14
+ end
15
+
16
+ def duration
17
+ end_time - start_time
18
+ end
19
+
20
+ def include?(tag)
21
+ tags.include? tag
22
+ end
23
+ end
24
+ end
data/lib/did/tag.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+
3
+ module Did
4
+ class Tag < ActiveRecord::Base
5
+
6
+ end
7
+ end
data/tests/all ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.dirname(__FILE__) + "/../lib"
4
+ require 'pathname'
5
+
6
+ def include_directory(dir)
7
+ dir.each_entry do |entry|
8
+ next if entry.to_s=~ /^\./
9
+ if (dir + entry).directory?
10
+ include_directory(dir + entry)
11
+ next
12
+ end
13
+ next unless entry.to_s=~ /^tc_.+\.rb$/
14
+
15
+ load dir + entry
16
+ end
17
+ end
18
+
19
+ include_directory(Pathname.new(__FILE__).dirname)
data/tests/helpers.rb ADDED
@@ -0,0 +1,37 @@
1
+
2
+
3
+ def perform_action(action, params = {})
4
+ action_class = Did::Action.const_get(action.to_s.camelize)
5
+ action = action_class.new
6
+ params.each do |key, value|
7
+ setter = (key.to_s + "=").to_sym
8
+ action.send(setter, value)
9
+ end
10
+ action.perform
11
+ end
12
+
13
+
14
+ def assert_correct_sitting_bounds(spans)
15
+ prior_sitting_id = nil
16
+ sitting_index = 0
17
+ sitting_span_index = 0
18
+ 0.upto(spans.length - 1) do |index|
19
+ sitting_span_index+= 1
20
+ span = spans[index]
21
+ next_span = spans[index + 1]
22
+ sitting_start = span.sitting_id != prior_sitting_id
23
+ sitting_end = next_span.nil? || next_span.sitting_id != span.sitting_id
24
+
25
+ location = "#{sitting_span_index}th span in #{sitting_index}th sitting"
26
+ assert_equal(sitting_start, span.sitting_start, "#{location} sitting_start")
27
+ assert_equal(sitting_end, span.sitting_end, "#{location} sitting_start")
28
+ assert_equal(span.id, span.sitting.end_span_id, "sitting.end_span of #{sitting_index}th sitting") if sitting_end
29
+
30
+ if prior_sitting_id != span.sitting_id
31
+ prior_sitting_id = span.sitting_id
32
+ sitting_span_index = 0
33
+ sitting_index+= 1
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,146 @@
1
+ $: << File.dirname(__FILE__) + "/../.."
2
+ require 'tests/setup'
3
+
4
+ require 'did/action/sit'
5
+ require 'did/action/submit'
6
+
7
+ class TCSitPerform < Test::Unit::TestCase
8
+
9
+ def test_first_span_no_prior_sitting
10
+ start_time = Time.local(2011,05,21, 10,00,00)
11
+ perform_action(:submit, :labels => [],
12
+ :start_time => start_time,
13
+ :end_time => start_time + 10.minutes)
14
+
15
+ spans = Did::Span.find(:all, :order => :start_time)
16
+ sittings = Did::Sitting.find(:all, :order => :start_time)
17
+
18
+ assert_equal(1, spans.length, "Should be one span")
19
+ assert_equal(1, sittings.length, "Should be one sitting")
20
+ assert_equal(true, sittings[0].current?, "Should mark sitting as current")
21
+ assert_equal(spans[0].start_time, sittings[0].start_time, "Should have matching start_times")
22
+ assert_equal(spans[0].end_time, sittings[0].end_time, "Should have matching start_times")
23
+
24
+ assert_correct_sitting_bounds(spans)
25
+ end
26
+
27
+ def test_prior_submit_too_old
28
+ start_time = Time.local(2011,05,21, 10,00,00)
29
+ perform_action(:submit, :labels => ['one'],
30
+ :start_time => start_time,
31
+ :end_time => start_time + 10.minutes)
32
+ perform_action(:submit, :labels => ['two'],
33
+ :start_time => start_time + 40.minutes,
34
+ :end_time => start_time + 50.minutes)
35
+
36
+ spans = Did::Span.find(:all, :order => :start_time)
37
+ sittings = Did::Sitting.find(:all, :order => :start_time)
38
+
39
+ assert_equal(2, sittings.length, "Should be two sittings")
40
+ assert_equal(sittings[0], spans[0].sitting, "Should match first span to first sitting")
41
+ assert_equal(sittings[1], spans[1].sitting, "Should match second span to second sitting")
42
+ assert_equal(false, sittings[0].current?, "Should mark first sitting as not current")
43
+ assert_equal(true, sittings[1].current?, "Should mark second sitting as current")
44
+
45
+ assert_correct_sitting_bounds(spans)
46
+ end
47
+
48
+
49
+ def test_prior_submit_recent
50
+ start_time = Time.local(2011,05,21, 10,00,00)
51
+ perform_action(:submit, :labels => ['one'],
52
+ :start_time => start_time,
53
+ :end_time => start_time + 10.minutes)
54
+ perform_action(:submit, :labels => ['two'],
55
+ :start_time => start_time + 5.minutes,
56
+ :end_time => start_time + 15.minutes)
57
+
58
+ spans = Did::Span.find(:all, :order => :start_time)
59
+ sittings = Did::Sitting.find(:all, :order => :start_time)
60
+
61
+ assert_equal(1, sittings.length, "Should be one sitting")
62
+ assert_equal(sittings[0], spans[0].sitting, "Should match first span to first sitting")
63
+ assert_equal(sittings[0], spans[1].sitting, "Should match second span to first sitting")
64
+ assert_equal(true, sittings[0].current?, "Should mark first sitting as current")
65
+
66
+ assert_correct_sitting_bounds(spans)
67
+ end
68
+
69
+ def test_prior_sit_recent
70
+ start_time = Time.local(2011,05,21, 10,00,00)
71
+ perform_action(:sit,
72
+ :start_time => start_time)
73
+ perform_action(:submit, :labels => [],
74
+ :start_time => start_time - 5.minutes,
75
+ :end_time => start_time + 5.minutes)
76
+
77
+ spans = Did::Span.find(:all, :order => :start_time)
78
+ sittings = Did::Sitting.find(:all, :order => :start_time)
79
+
80
+ assert_equal(1, sittings.length, "Should be one sitting")
81
+ assert_equal(start_time, spans[0].start_time, "Span should use sit's start time")
82
+ assert_equal(start_time + 5.minutes, sittings[0].end_time, "Sitting should use span's end time")
83
+ assert_equal(true, sittings[0].current?, "Should mark sitting as current")
84
+
85
+ assert_correct_sitting_bounds(spans)
86
+ end
87
+
88
+
89
+ def test_prior_sit_too_old
90
+ start_time = Time.local(2011,05,21, 10,00,00)
91
+ perform_action(:sit,
92
+ :start_time => start_time)
93
+ perform_action(:submit, :labels => [],
94
+ :start_time => start_time + 25.minutes,
95
+ :end_time => start_time + 35.minutes)
96
+
97
+ spans = Did::Span.find(:all, :order => :start_time)
98
+ sittings = Did::Sitting.find(:all, :order => :start_time)
99
+
100
+ assert_equal(1, sittings.length, "Should be one sitting")
101
+ assert_equal(start_time + 25.minutes, spans[0].start_time, "Span should use span's start time")
102
+ assert_equal(start_time + 35.minutes, sittings[0].end_time, "Sitting should use span's end time")
103
+ assert_equal(true, sittings[0].current?, "Should mark sitting as current")
104
+
105
+ assert_correct_sitting_bounds(spans)
106
+ end
107
+
108
+ def test_sit_after_submit
109
+ start_time = Time.local(2011,05,21, 10,00,00)
110
+ perform_action(:submit, :labels => [],
111
+ :start_time => start_time,
112
+ :end_time => start_time + 10.minutes)
113
+ perform_action(:sit,
114
+ :start_time => start_time + 15.minutes)
115
+ perform_action(:submit, :labels => [],
116
+ :start_time => start_time + 8.minutes,
117
+ :end_time => start_time + 18.minutes)
118
+
119
+ spans = Did::Span.find(:all, :order => :start_time)
120
+ sittings = Did::Sitting.find(:all, :order => :start_time)
121
+
122
+ assert_equal(2, sittings.length, "Should be two sittings")
123
+ assert_equal(start_time + 15.minutes, sittings[1].start_time)
124
+ assert_equal(start_time + 18.minutes, sittings[1].end_time)
125
+
126
+ assert_correct_sitting_bounds(spans)
127
+ end
128
+
129
+ def test_double_sit
130
+ start_time = Time.local(2011,05,21, 10,00,00)
131
+ perform_action(:sit,
132
+ :start_time => start_time)
133
+ perform_action(:sit,
134
+ :start_time => start_time + 1.minute)
135
+
136
+ sittings = Did::Sitting.find(:all, :order => :start_time)
137
+
138
+ assert_equal(1, sittings.length, "Should be one sitting")
139
+ end
140
+
141
+
142
+
143
+ def teardown
144
+ DatabaseCleaner.clean
145
+ end
146
+ end
@@ -0,0 +1,45 @@
1
+ $: << File.dirname(__FILE__) + "/../.."
2
+ require 'tests/setup'
3
+
4
+ class TCTags < Test::Unit::TestCase
5
+
6
+ def test_create_tags
7
+ start_time = Time.local(2011,05,21, 10,00,00)
8
+ perform_action(:submit, :labels => ['one'],
9
+ :start_time => start_time,
10
+ :end_time => start_time + 10.minutes)
11
+
12
+ tags = Did::Tag.find(:all)
13
+ assert_equal(1, tags.length, "Should create a tag")
14
+ end
15
+
16
+ def test_no_duplicate_tags
17
+ start_time = Time.local(2011,05,21, 10,00,00)
18
+ perform_action(:submit, :labels => ['one'],
19
+ :start_time => start_time,
20
+ :end_time => start_time + 10.minutes)
21
+ perform_action(:submit, :labels => ['one'],
22
+ :start_time => start_time + 11,
23
+ :end_time => start_time + 21.minutes)
24
+
25
+ tags = Did::Tag.find(:all)
26
+ assert_equal(1, tags.length, "Should create only one tag")
27
+ end
28
+
29
+ def test_contiguous_identical_tags
30
+ start_time = Time.local(2011,05,21, 10,00,00)
31
+ perform_action(:submit, :labels => ['one'],
32
+ :start_time => start_time,
33
+ :end_time => start_time + 10.minutes)
34
+ perform_action(:submit, :labels => ['one'],
35
+ :start_time => start_time + 20,
36
+ :end_time => start_time + 30.minutes)
37
+
38
+ spans = Did::Span.find(:all, :order => :start_time)
39
+ assert_equal(1, spans.length, "Should reuse first span")
40
+ end
41
+
42
+ def teardown
43
+ DatabaseCleaner.clean
44
+ end
45
+ end
data/tests/profile.rb ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.realpath(File.dirname(__FILE__) + "/../lib")
4
+ $: << File.dirname(__FILE__) + "/.."
5
+ ENVIRONMENT_NAME = 'test'
6
+
7
+ require 'did'
8
+ require 'tests/helpers'
9
+ require 'database_cleaner'
10
+ DatabaseCleaner.strategy = :truncation
11
+
12
+
13
+ WORD_COUNT = 50
14
+ SITTING_COUNT = 10
15
+ SPAN_COUNT_PER_SITTING = 10
16
+
17
+
18
+ dictionary_file = File.dirname(__FILE__) + "/../notes/dictionary_short"
19
+ words = `cat #{dictionary_file} | head -n #{WORD_COUNT}`.split("\n")
20
+ word_index = 0
21
+
22
+ time = Time.local(2001,01,01, 12,01,01)
23
+ times = []
24
+
25
+ 0.upto(SITTING_COUNT) do
26
+ times << time += 8.minutes
27
+ 0.upto(SPAN_COUNT_PER_SITTING) do
28
+ times << time += 8.minutes
29
+ end
30
+ end
31
+
32
+ DatabaseCleaner.clean
33
+ require 'perftools'
34
+ start_time = Time.now
35
+ puts "starting profile: #{times.length} actions"
36
+ PerfTools::CpuProfiler.start("profile/many_spans") do
37
+ times.each_with_index do |time, index|
38
+ if (index % (SPAN_COUNT_PER_SITTING + 1) == 0)
39
+ action = Did::Action.parse(["sit"])
40
+ action.perform
41
+ else
42
+ action = Did::Action.parse([words[index % WORD_COUNT]])
43
+ action.perform
44
+ end
45
+ end
46
+ end
47
+ end_time = Time.now
48
+
49
+ puts "#{times.length} actions (#{WORD_COUNT} words, #{SPAN_COUNT_PER_SITTING}:1 span:sit) - " +
50
+ "#{end_time - start_time} #{(end_time - start_time) / times.length} per action"
data/tests/setup.rb ADDED
@@ -0,0 +1,12 @@
1
+ $: << File.realpath(File.dirname(__FILE__) + "/../lib")
2
+ ENVIRONMENT_NAME = 'test'
3
+
4
+ require 'test/unit'
5
+ require 'did/env'
6
+
7
+ require 'tests/helpers'
8
+
9
+ require 'database_cleaner'
10
+ DatabaseCleaner.strategy = :truncation
11
+ DatabaseCleaner.clean
12
+
@@ -0,0 +1,34 @@
1
+ $: << File.dirname(__FILE__) + "/../.."
2
+ require 'tests/setup'
3
+
4
+ require 'did/span'
5
+
6
+ class TCSpanFindForDay < Test::Unit::TestCase
7
+
8
+ def test_date_range
9
+ today = Date.civil(2011,10,05)
10
+
11
+ span_1 = Did::Span.create(:start_time => today - 1.hour,
12
+ :end_time => today - 50.minutes)
13
+ span_2 = Did::Span.create(:start_time => today - 5.minutes,
14
+ :end_time => today + 5.minutes)
15
+ span_3 = Did::Span.create(:start_time => today + 15.minutes,
16
+ :end_time => today + 25.minutes)
17
+ span_4 = Did::Span.create(:start_time => today + 23.hours + 55.minutes,
18
+ :end_time => today + 1.day + 5.minutes)
19
+ span_5 = Did::Span.create(:start_time => today + 1.day + 10.minutes,
20
+ :end_time => today + 1.day + 20.minutes)
21
+
22
+ spans = Did::Span.find_for_day(today)
23
+
24
+ assert_equal(span_2, spans[0])
25
+ assert_equal(span_3, spans[1])
26
+ assert_equal(span_4, spans[2])
27
+ assert_equal(3, spans.length)
28
+ end
29
+
30
+
31
+ def teardown
32
+ DatabaseCleaner.clean
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: did
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.01
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Paradise
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-25 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sqlite3
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: "3.0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ description: |
38
+ DID is a time tracking system where you tell it what you did, not what
39
+ you plan to do.
40
+
41
+ email: adparadise@gmail.com
42
+ executables:
43
+ - did
44
+ - did_autocomplete
45
+ extensions: []
46
+
47
+ extra_rdoc_files: []
48
+
49
+ files:
50
+ - db/migrations/20110524024714_add_sitting_tags.rb
51
+ - db/migrations/20110521021941_create_work_tables.rb
52
+ - db/migrations/20110521000254_create_tag_table.rb
53
+ - db/migrations/20110521174213_add_sitting_start_end_to_span.rb
54
+ - db/migrations/20110521134514_create_sitting_table.rb
55
+ - db/migrations/20110521153703_add_current_flag_to_sitting.rb
56
+ - db/config.yml
57
+ - db/schema.rb
58
+ - db/schema.sql
59
+ - lib/did/env.rb
60
+ - lib/did/span.rb
61
+ - lib/did/action/report.rb
62
+ - lib/did/action/submit.rb
63
+ - lib/did/action/sit.rb
64
+ - lib/did/action.rb
65
+ - lib/did/tag.rb
66
+ - lib/did/sitting.rb
67
+ - lib/did.rb
68
+ - tests/integration/tc_sit_perform.rb
69
+ - tests/integration/tc_tags.rb
70
+ - tests/setup.rb
71
+ - tests/helpers.rb
72
+ - tests/profile.rb
73
+ - tests/all
74
+ - tests/unit/tc_span_find_for_day.rb
75
+ - README.rdoc
76
+ - bin/did
77
+ - bin/did_autocomplete
78
+ homepage: http://github.com/adparadise/did
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.3
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Time tracking for the non-clairvoyant
105
+ test_files: []
106
+