trello_lead_time 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module TrelloLeadTime
2
+ VERSION = '0.0.1'
3
+ 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
+ ]