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 +52 -0
- data/bin/did +8 -0
- data/bin/did_autocomplete +12 -0
- data/db/config.yml +5 -0
- data/db/migrations/20110521000254_create_tag_table.rb +13 -0
- data/db/migrations/20110521021941_create_work_tables.rb +20 -0
- data/db/migrations/20110521134514_create_sitting_table.rb +14 -0
- data/db/migrations/20110521153703_add_current_flag_to_sitting.rb +11 -0
- data/db/migrations/20110521174213_add_sitting_start_end_to_span.rb +13 -0
- data/db/migrations/20110524024714_add_sitting_tags.rb +9 -0
- data/db/schema.rb +45 -0
- data/db/schema.sql +9 -0
- data/lib/did.rb +6 -0
- data/lib/did/action.rb +44 -0
- data/lib/did/action/report.rb +69 -0
- data/lib/did/action/sit.rb +20 -0
- data/lib/did/action/submit.rb +74 -0
- data/lib/did/env.rb +80 -0
- data/lib/did/sitting.rb +10 -0
- data/lib/did/span.rb +24 -0
- data/lib/did/tag.rb +7 -0
- data/tests/all +19 -0
- data/tests/helpers.rb +37 -0
- data/tests/integration/tc_sit_perform.rb +146 -0
- data/tests/integration/tc_tags.rb +45 -0
- data/tests/profile.rb +50 -0
- data/tests/setup.rb +12 -0
- data/tests/unit/tc_span_find_for_day.rb +34 -0
- metadata +106 -0
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,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,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,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
|
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
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
|
data/lib/did/sitting.rb
ADDED
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
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,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
|
+
|