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