did 0.2.01
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|