trello_lead_time 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.md +21 -0
- data/README.md +88 -0
- data/Rakefile +1 -0
- data/lib/trello_lead_time.rb +10 -0
- data/lib/trello_lead_time/action.rb +23 -0
- data/lib/trello_lead_time/board.rb +58 -0
- data/lib/trello_lead_time/card.rb +47 -0
- data/lib/trello_lead_time/config.rb +36 -0
- data/lib/trello_lead_time/list.rb +69 -0
- data/lib/trello_lead_time/time_humanizer.rb +13 -0
- data/lib/trello_lead_time/timeline.rb +142 -0
- data/lib/trello_lead_time/version.rb +3 -0
- data/sample.rb +33 -0
- data/spec/fixtures/actions.1111.json +146 -0
- data/spec/fixtures/actions.2222.json +146 -0
- data/spec/fixtures/actions.3333.json +146 -0
- data/spec/fixtures/actions.4444.json +146 -0
- data/spec/fixtures/boards.json +112 -0
- data/spec/fixtures/card.json +45 -0
- data/spec/fixtures/cards.json +18 -0
- data/spec/fixtures/lists.json +10 -0
- data/spec/fixtures/organization.json +12 -0
- data/spec/lib/trello_lead_time/board_spec.rb +115 -0
- data/spec/lib/trello_lead_time/card_spec.rb +24 -0
- data/spec/lib/trello_lead_time/config_spec.rb +50 -0
- data/spec/lib/trello_lead_time/list_spec.rb +6 -0
- data/spec/lib/trello_lead_time/time_humanizer_spec.rb +12 -0
- data/spec/lib/trello_lead_time_spec.rb +9 -0
- data/spec/spec_helper.rb +14 -0
- data/trello_lead_time.gemspec +23 -0
- metadata +124 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module TrelloLeadTime
|
2
|
+
class TimeHumanizer
|
3
|
+
def self.humanize_seconds(seconds)
|
4
|
+
[[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].inject([]){ |s, (count, name)|
|
5
|
+
if seconds > 0
|
6
|
+
seconds, n = seconds.divmod(count)
|
7
|
+
s.unshift "#{n.to_i} #{name}"
|
8
|
+
end
|
9
|
+
s
|
10
|
+
}.join(' ')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module TrelloLeadTime
|
2
|
+
class Timeline
|
3
|
+
def self.for_trello_card(trello_card)
|
4
|
+
Timeline.new(trello_card)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(trello_card)
|
8
|
+
@trello_card = trello_card
|
9
|
+
assemble_timeline
|
10
|
+
end
|
11
|
+
|
12
|
+
def done?
|
13
|
+
!last_done_action.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def closed?
|
17
|
+
!closed_date.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def closed_date
|
21
|
+
action = actions.detect {|a| a.type =~ /updateCard/i && a.data["old"].has_key?("closed")}
|
22
|
+
action ? action.date : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def age_in_seconds
|
26
|
+
@_age ||= calculate_age_in_seconds(creation_date, done_date)
|
27
|
+
end
|
28
|
+
|
29
|
+
def done_date
|
30
|
+
last_done_action.date if !last_done_action.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def seconds_in_list(list_name)
|
34
|
+
matched_list = @_seconds_in_list.values.detect { |list| list[:list_name] == list_name }
|
35
|
+
return matched_list[:seconds_in_list] if !matched_list.nil?
|
36
|
+
0
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def last_done_action
|
42
|
+
@_last_done_action ||= actions.detect { |a| a.type =~ /updateCard/ && a.data.has_key?("listAfter") && a.data["listAfter"].has_key?("name") && a.data["listAfter"]["name"] =~ Config.list_name_matcher_for_done }
|
43
|
+
end
|
44
|
+
|
45
|
+
def actions
|
46
|
+
@_actions ||= filtered_actions
|
47
|
+
end
|
48
|
+
|
49
|
+
def filtered_actions
|
50
|
+
@trello_card.actions(filter: 'createCard,updateCard:idList,updateCard:closed').map { |action| Action.from_trello_action(action) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def creation_date
|
54
|
+
return actions.last.date if actions.size > 0
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def calculate_age_in_seconds(start_time, end_time)
|
59
|
+
(end_time - start_time).round(0)
|
60
|
+
end
|
61
|
+
|
62
|
+
def assemble_timeline
|
63
|
+
@_seconds_in_list = {}
|
64
|
+
actions.reverse.each_with_index do |action, index|
|
65
|
+
before_list = list_before_current_index(index)
|
66
|
+
after_list = list_from_result_of_action(action)
|
67
|
+
seconds = time_since_last_action(index)
|
68
|
+
|
69
|
+
list_identifier = before_list["id"]
|
70
|
+
if list_identifier
|
71
|
+
if !@_seconds_in_list.has_key?(list_identifier)
|
72
|
+
@_seconds_in_list[list_identifier] = {
|
73
|
+
list_id: before_list["id"],
|
74
|
+
list_name: before_list["name"],
|
75
|
+
seconds_in_list: 0
|
76
|
+
}
|
77
|
+
end
|
78
|
+
@_seconds_in_list[list_identifier][:seconds_in_list] += seconds
|
79
|
+
end
|
80
|
+
|
81
|
+
if index == actions.length - 1
|
82
|
+
# we won't have an after_list if the card was closed after being
|
83
|
+
# created and left in a single list
|
84
|
+
if after_list
|
85
|
+
list_identifier = after_list["id"]
|
86
|
+
if !@_seconds_in_list.has_key?(list_identifier)
|
87
|
+
@_seconds_in_list[list_identifier] = {
|
88
|
+
list_id: after_list["id"],
|
89
|
+
list_name: after_list["name"],
|
90
|
+
seconds_in_list: 0
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
seconds = 0
|
95
|
+
if done?
|
96
|
+
seconds = calculate_age_in_seconds(action.date, done_date)
|
97
|
+
elsif closed?
|
98
|
+
seconds = calculate_age_in_seconds(action.date, closed_date)
|
99
|
+
else
|
100
|
+
seconds = calculate_age_in_seconds(action.date, Time.now)
|
101
|
+
end
|
102
|
+
@_seconds_in_list[list_identifier][:seconds_in_list] += seconds
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def list_before_current_index(current_index)
|
110
|
+
if current_index == 0
|
111
|
+
"?"
|
112
|
+
else
|
113
|
+
list = nil
|
114
|
+
current_index.downto(1) do |i|
|
115
|
+
list = list_from_result_of_action(actions.reverse[i - 1])
|
116
|
+
break if list
|
117
|
+
end
|
118
|
+
list
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def list_from_result_of_action(action)
|
123
|
+
if action.type == "createCard"
|
124
|
+
action.data["list"]
|
125
|
+
elsif action.type == "updateCard" && action.data.has_key?("old") && action.data["old"].has_key?("idList")
|
126
|
+
action.data["listAfter"]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def time_since_last_action(current_index)
|
131
|
+
if current_index == 0
|
132
|
+
0
|
133
|
+
else
|
134
|
+
acts = actions.reverse
|
135
|
+
current_date = acts[current_index].date
|
136
|
+
previous_date = acts[current_index - 1].date
|
137
|
+
(current_date - previous_date).round(0)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
data/sample.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'trello_lead_time'
|
2
|
+
|
3
|
+
developer_public_key = 'YOUR TRELLO PUBLIC KEY'
|
4
|
+
member_token = 'YOUR TRELLO MEMBER TOKEN'
|
5
|
+
organization_name = 'fogcreek'
|
6
|
+
board_url = 'https://trello.com/b/nC8QJJoZ/trello-development'
|
7
|
+
source_lists = ['Live (4/8)', 'Live (3/17)', 'Live (3/3)', 'Live (2/11)', 'Live (1/14)']
|
8
|
+
queue_time_lists = ['Next Up']
|
9
|
+
cycle_time_lists = ['In Progress', 'Testing']
|
10
|
+
list_name_matcher_for_done = /^Live/
|
11
|
+
|
12
|
+
TrelloLeadTime.configure do |cfg|
|
13
|
+
cfg.organization_name = organization_name
|
14
|
+
cfg.set_trello_key_and_token(developer_public_key, member_token)
|
15
|
+
cfg.queue_time_lists = queue_time_lists
|
16
|
+
cfg.cycle_time_lists = cycle_time_lists
|
17
|
+
cfg.list_name_matcher_for_done = list_name_matcher_for_done
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "-" * 40
|
21
|
+
puts "Calculating metrics for:"
|
22
|
+
puts "#{board_url}"
|
23
|
+
puts "-" * 40
|
24
|
+
|
25
|
+
board = TrelloLeadTime::Board.from_url board_url
|
26
|
+
source_lists.each do |source_list|
|
27
|
+
puts "Using cards in list: #{source_list}"
|
28
|
+
puts "Average Card Age: #{TrelloLeadTime::TimeHumanizer.humanize_seconds(board.average_age(source_list))}"
|
29
|
+
puts "Average Lead Time: #{TrelloLeadTime::TimeHumanizer.humanize_seconds(board.average_lead_time(source_list))}"
|
30
|
+
puts "Average Queue Time: #{TrelloLeadTime::TimeHumanizer.humanize_seconds(board.average_queue_time(source_list))}"
|
31
|
+
puts "Average Cycle Time: #{TrelloLeadTime::TimeHumanizer.humanize_seconds(board.average_cycle_time(source_list))}"
|
32
|
+
puts ""
|
33
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": "1234",
|
4
|
+
"idMemberCreator": "1234",
|
5
|
+
"data": {
|
6
|
+
"listAfter": {
|
7
|
+
"name": "Done for 2014-04-11",
|
8
|
+
"id": "3333"
|
9
|
+
},
|
10
|
+
"listBefore": {
|
11
|
+
"name": "Acceptance",
|
12
|
+
"id": "5555"
|
13
|
+
},
|
14
|
+
"board": {
|
15
|
+
"shortLink": "1234",
|
16
|
+
"name": "Transp Backend",
|
17
|
+
"id": "1234"
|
18
|
+
},
|
19
|
+
"card": {
|
20
|
+
"shortLink": "1234",
|
21
|
+
"idShort": 1006,
|
22
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
23
|
+
"id": "1234",
|
24
|
+
"idList": "1234"
|
25
|
+
},
|
26
|
+
"old": {
|
27
|
+
"idList": "1234"
|
28
|
+
}
|
29
|
+
},
|
30
|
+
"type": "updateCard",
|
31
|
+
"date": "2014-04-08T20:14:08.000Z",
|
32
|
+
"memberCreator": {
|
33
|
+
"id": "1234",
|
34
|
+
"avatarHash": "1234",
|
35
|
+
"fullName": "Connor Edstrom",
|
36
|
+
"initials": "CE",
|
37
|
+
"username": "connoredstrom"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
{
|
41
|
+
"id": "1234",
|
42
|
+
"idMemberCreator": "1234",
|
43
|
+
"data": {
|
44
|
+
"listAfter": {
|
45
|
+
"name": "Acceptance",
|
46
|
+
"id": "5555"
|
47
|
+
},
|
48
|
+
"listBefore": {
|
49
|
+
"name": "Development",
|
50
|
+
"id": "2222"
|
51
|
+
},
|
52
|
+
"board": {
|
53
|
+
"shortLink": "1234",
|
54
|
+
"name": "Transp Backend",
|
55
|
+
"id": "1234"
|
56
|
+
},
|
57
|
+
"card": {
|
58
|
+
"shortLink": "1234",
|
59
|
+
"idShort": 1006,
|
60
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
61
|
+
"id": "1234",
|
62
|
+
"idList": "1234"
|
63
|
+
},
|
64
|
+
"old": {
|
65
|
+
"idList": "1234"
|
66
|
+
}
|
67
|
+
},
|
68
|
+
"type": "updateCard",
|
69
|
+
"date": "2014-04-07T20:14:08.000Z",
|
70
|
+
"memberCreator": {
|
71
|
+
"id": "1234",
|
72
|
+
"avatarHash": "1234",
|
73
|
+
"fullName": "Connor Edstrom",
|
74
|
+
"initials": "CE",
|
75
|
+
"username": "connoredstrom"
|
76
|
+
}
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"id": "1234",
|
80
|
+
"idMemberCreator": "1234",
|
81
|
+
"data": {
|
82
|
+
"listAfter": {
|
83
|
+
"name": "Development",
|
84
|
+
"id": "2222"
|
85
|
+
},
|
86
|
+
"listBefore": {
|
87
|
+
"name": "Product Backlog",
|
88
|
+
"id": "1111"
|
89
|
+
},
|
90
|
+
"board": {
|
91
|
+
"shortLink": "1234",
|
92
|
+
"name": "Transp Backend",
|
93
|
+
"id": "1234"
|
94
|
+
},
|
95
|
+
"card": {
|
96
|
+
"shortLink": "1234",
|
97
|
+
"idShort": 1006,
|
98
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
99
|
+
"id": "1234",
|
100
|
+
"idList": "1234"
|
101
|
+
},
|
102
|
+
"old": {
|
103
|
+
"idList": "1234"
|
104
|
+
}
|
105
|
+
},
|
106
|
+
"type": "updateCard",
|
107
|
+
"date": "2014-04-07T16:14:08.000Z",
|
108
|
+
"memberCreator": {
|
109
|
+
"id": "1234",
|
110
|
+
"avatarHash": "1234",
|
111
|
+
"fullName": "Connor Edstrom",
|
112
|
+
"initials": "CE",
|
113
|
+
"username": "connoredstrom"
|
114
|
+
}
|
115
|
+
},
|
116
|
+
{
|
117
|
+
"id": "1234",
|
118
|
+
"idMemberCreator": "1234",
|
119
|
+
"data": {
|
120
|
+
"board": {
|
121
|
+
"shortLink": "1234",
|
122
|
+
"name": "Transp Backend",
|
123
|
+
"id": "1234"
|
124
|
+
},
|
125
|
+
"list": {
|
126
|
+
"name": "Product Backlog",
|
127
|
+
"id": "1111"
|
128
|
+
},
|
129
|
+
"card": {
|
130
|
+
"shortLink": "1234",
|
131
|
+
"idShort": 1006,
|
132
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
133
|
+
"id": "1234"
|
134
|
+
}
|
135
|
+
},
|
136
|
+
"type": "createCard",
|
137
|
+
"date": "2014-04-03T21:18:13.000Z",
|
138
|
+
"memberCreator": {
|
139
|
+
"id": "1234",
|
140
|
+
"avatarHash": "1234",
|
141
|
+
"fullName": "Connor Edstrom",
|
142
|
+
"initials": "CE",
|
143
|
+
"username": "connoredstrom"
|
144
|
+
}
|
145
|
+
}
|
146
|
+
]
|
@@ -0,0 +1,146 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": "1234",
|
4
|
+
"idMemberCreator": "1234",
|
5
|
+
"data": {
|
6
|
+
"listAfter": {
|
7
|
+
"name": "Done for 2014-04-11",
|
8
|
+
"id": "3333"
|
9
|
+
},
|
10
|
+
"listBefore": {
|
11
|
+
"name": "Acceptance",
|
12
|
+
"id": "5555"
|
13
|
+
},
|
14
|
+
"board": {
|
15
|
+
"shortLink": "1234",
|
16
|
+
"name": "Transp Backend",
|
17
|
+
"id": "1234"
|
18
|
+
},
|
19
|
+
"card": {
|
20
|
+
"shortLink": "1234",
|
21
|
+
"idShort": 1006,
|
22
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
23
|
+
"id": "1234",
|
24
|
+
"idList": "1234"
|
25
|
+
},
|
26
|
+
"old": {
|
27
|
+
"idList": "1234"
|
28
|
+
}
|
29
|
+
},
|
30
|
+
"type": "updateCard",
|
31
|
+
"date": "2014-04-08T18:50:15.000Z",
|
32
|
+
"memberCreator": {
|
33
|
+
"id": "1234",
|
34
|
+
"avatarHash": "1234",
|
35
|
+
"fullName": "Connor Edstrom",
|
36
|
+
"initials": "CE",
|
37
|
+
"username": "connoredstrom"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
{
|
41
|
+
"id": "1234",
|
42
|
+
"idMemberCreator": "1234",
|
43
|
+
"data": {
|
44
|
+
"listAfter": {
|
45
|
+
"name": "Acceptance",
|
46
|
+
"id": "5555"
|
47
|
+
},
|
48
|
+
"listBefore": {
|
49
|
+
"name": "Development",
|
50
|
+
"id": "2222"
|
51
|
+
},
|
52
|
+
"board": {
|
53
|
+
"shortLink": "1234",
|
54
|
+
"name": "Transp Backend",
|
55
|
+
"id": "1234"
|
56
|
+
},
|
57
|
+
"card": {
|
58
|
+
"shortLink": "1234",
|
59
|
+
"idShort": 1006,
|
60
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
61
|
+
"id": "1234",
|
62
|
+
"idList": "1234"
|
63
|
+
},
|
64
|
+
"old": {
|
65
|
+
"idList": "1234"
|
66
|
+
}
|
67
|
+
},
|
68
|
+
"type": "updateCard",
|
69
|
+
"date": "2014-04-08T11:45:00.000Z",
|
70
|
+
"memberCreator": {
|
71
|
+
"id": "1234",
|
72
|
+
"avatarHash": "1234",
|
73
|
+
"fullName": "Connor Edstrom",
|
74
|
+
"initials": "CE",
|
75
|
+
"username": "connoredstrom"
|
76
|
+
}
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"id": "1234",
|
80
|
+
"idMemberCreator": "1234",
|
81
|
+
"data": {
|
82
|
+
"listAfter": {
|
83
|
+
"name": "Development",
|
84
|
+
"id": "2222"
|
85
|
+
},
|
86
|
+
"listBefore": {
|
87
|
+
"name": "Product Backlog",
|
88
|
+
"id": "1111"
|
89
|
+
},
|
90
|
+
"board": {
|
91
|
+
"shortLink": "1234",
|
92
|
+
"name": "Transp Backend",
|
93
|
+
"id": "1234"
|
94
|
+
},
|
95
|
+
"card": {
|
96
|
+
"shortLink": "1234",
|
97
|
+
"idShort": 1006,
|
98
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
99
|
+
"id": "1234",
|
100
|
+
"idList": "1234"
|
101
|
+
},
|
102
|
+
"old": {
|
103
|
+
"idList": "1234"
|
104
|
+
}
|
105
|
+
},
|
106
|
+
"type": "updateCard",
|
107
|
+
"date": "2014-04-05T08:15:00.000Z",
|
108
|
+
"memberCreator": {
|
109
|
+
"id": "1234",
|
110
|
+
"avatarHash": "1234",
|
111
|
+
"fullName": "Connor Edstrom",
|
112
|
+
"initials": "CE",
|
113
|
+
"username": "connoredstrom"
|
114
|
+
}
|
115
|
+
},
|
116
|
+
{
|
117
|
+
"id": "1234",
|
118
|
+
"idMemberCreator": "1234",
|
119
|
+
"data": {
|
120
|
+
"board": {
|
121
|
+
"shortLink": "1234",
|
122
|
+
"name": "Transp Backend",
|
123
|
+
"id": "1234"
|
124
|
+
},
|
125
|
+
"list": {
|
126
|
+
"name": "Product Backlog",
|
127
|
+
"id": "1111"
|
128
|
+
},
|
129
|
+
"card": {
|
130
|
+
"shortLink": "1234",
|
131
|
+
"idShort": 1006,
|
132
|
+
"name": "SEARCH 2.0: Hit HL typeahead and apply filtering/fallbacks",
|
133
|
+
"id": "1234"
|
134
|
+
}
|
135
|
+
},
|
136
|
+
"type": "createCard",
|
137
|
+
"date": "2014-04-03T07:00:00.000Z",
|
138
|
+
"memberCreator": {
|
139
|
+
"id": "1234",
|
140
|
+
"avatarHash": "1234",
|
141
|
+
"fullName": "Connor Edstrom",
|
142
|
+
"initials": "CE",
|
143
|
+
"username": "connoredstrom"
|
144
|
+
}
|
145
|
+
}
|
146
|
+
]
|