did 0.2.01

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