trollolo 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Trollolo
2
+
3
+ VERSION = "0.0.3"
4
+
5
+ end
@@ -0,0 +1,4 @@
1
+ *.1
2
+ *.5
3
+ *.8
4
+ *.html
@@ -0,0 +1,152 @@
1
+ # Trollolo -- Trello command line client
2
+
3
+ ## SYNOPSIS
4
+
5
+ `trollolo` [command] [options]
6
+
7
+ `trollolo` help [command]
8
+
9
+
10
+ ## DESCRIPTION
11
+
12
+ **Trollolo** is a command line client for Trello. It supports fetching lists
13
+ and cards and has functionality for extracting data for burndown charts.
14
+
15
+
16
+ ## GENERAL OPTIONS
17
+
18
+ * `--version`:
19
+ Give out version of trollolo tool. Exit when done.
20
+
21
+ * `--verbose`:
22
+ Run in verbose mode.
23
+
24
+ * `--board-id`:
25
+ Most commands take a `board-id` parameter. This is the id of the Trello
26
+ board. It is the cryptic part of the URL of the Trello board.
27
+
28
+ * `--raw`:
29
+ Some of the commands take a `raw` option. If this is provided the commands
30
+ put out the raw JSON returned by the server instead of processing it to
31
+ a more human-readable version.
32
+
33
+
34
+ ## COMMANDS
35
+
36
+ ### burndown-init -- Initialize burndown chart
37
+
38
+ `trollolo burndown-init --board-id=<board id> --output=<directory>`
39
+
40
+ Initialize the given directory for the generation of burndown charts. It stores
41
+ the given board id in the directory in a YAML file together with other
42
+ configuration data. The YAML file also is used to store the data for the
43
+ burndown charts. The `burndown` command can be used to update the file with
44
+ data from the specified Trello board.
45
+
46
+ The directory also gets a script to do the actual generation of the burndown
47
+ chart. Just run this script after each update of the data to get the latest
48
+ burndown chart.
49
+
50
+ ### burndown -- Process data for burndown chart
51
+
52
+ `trollolo burndown --output=<directory>`
53
+
54
+ Update the burndown data in the given directory from the Trello board
55
+ specified in the YAML file in the directory. The given directory has to be
56
+ initialized before running the `burndown` command by running the
57
+ `burndown-init` command.
58
+
59
+ The actual generation of the burndown chart is done by running the script
60
+ which is put into the directory by the `burndown-init` command.
61
+
62
+ For correct generation of the burndown chart, the Trello board has to follow
63
+ a few convention. They are described in the section `CONVENTIONS for SCRUM
64
+ BOARDS`.
65
+
66
+ ### plot -- Plot burndown chart
67
+
68
+ `trollolo plot <sprint-number>`
69
+
70
+ Plot the burndown chart for given sprint. This command assumes that you are in
71
+ the burndown directory (initially created with `burndown-init`) and that the
72
+ corresponding file `burndown-data-<sprint-number>.yaml` exists there.
73
+
74
+ ### fetch-burndown-data -- Read data for burndown chart
75
+
76
+ `trollolo fetch-burndown-data --board-id=<board id>`
77
+
78
+ Reads data from the specified Trello board, extracts it according to the
79
+ conventions for Scrum boards, and reports burndown data.
80
+
81
+ ### get-cards -- Get card data for a board
82
+
83
+ Read all card data for a given board.
84
+
85
+ ### get-checklists -- Get checklist data for a board
86
+
87
+ Read all checklist data for a given board
88
+
89
+ ### get-lists -- Get list data for a board
90
+
91
+ Read all list data for a given board.
92
+
93
+
94
+ ## EXAMPLES
95
+
96
+ Fetch burndown data of a Trello board configured in the configuration file:
97
+
98
+ `trollolo fetch-burndown-data --board-id=CRdddpdy`
99
+
100
+ Fetch raw data of all cards of a Trello board:
101
+
102
+ `trollolo get-cards --raw --board-id=CRdddpdy`
103
+
104
+
105
+ ## CONFIGURATION
106
+
107
+ Trollolo reads a configuration file `.trollolorc` in the home directory of the
108
+ user running the command line tool. It reads the data required to authenticate
109
+ with the Trello server from it. It's two values (the example shows random data):
110
+
111
+ ```yaml
112
+ developer_public_key: 87349873487ef8732487234
113
+ member_token: 87345897238957a29835789b2374580927f3589072398579820345
114
+ ```
115
+
116
+ These values have to be set with the personal access data for the Trello API
117
+ and the id of the board, which is processed.
118
+
119
+ For creating a developer key go to the
120
+ [Developer API Keys](https://trello.com/1/appKey/generate) page on Trello. It's
121
+ the key in the first box.
122
+
123
+ For creating a member token go follow the
124
+ [instructions](https://trello.com/docs/gettingstarted/index.html#getting-a-token-from-a-user)
125
+ in the Trello API documentation.
126
+
127
+ The board id is the cryptic string in the URL of your board.
128
+
129
+
130
+ ## CONVENTIONS FOR SCRUM BOARDS
131
+
132
+ The burndown functionality expects the board to follow a certain naming scheme,
133
+ so that Trollolo can process it as a Scrum board.
134
+
135
+ It expects a list `Sprint Backlog` with open items, a list `Doing` with items in
136
+ progress, and a list `Done Sprint X` with done items, where `X` is the number
137
+ of the sprint. For burndown data calculation the list with the highest number
138
+ is taken.
139
+
140
+ On work item cards the tool takes a bracketed number as suffix as size of the
141
+ item in story points. E.g. a card with the title `(3) Build magic tool` would
142
+ have three story points.
143
+
144
+ Cards under the waterline not part of the actual sprint commitment are expected
145
+ to have a label with the name "Under waterline".
146
+
147
+ An example for a board which follow this conventions is the [Trollolo Testing
148
+ Board](https://trello.com/b/CRdddpdy/trollolo-testing-board).
149
+
150
+ ## COPYRIGHT
151
+
152
+ Trollolo is Copyright (C) 2013-2014 SUSE LLC
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/python
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ import sys
5
+ import yaml
6
+
7
+ if len(sys.argv) != 2:
8
+ print "Usage: machinery-burndown.py <sprint-number>"
9
+ sys.exit(1)
10
+
11
+ sprint = sys.argv[1]
12
+
13
+ with open('burndown-data-' + sprint + '.yaml', 'r') as f:
14
+ burndown = yaml.load(f)
15
+
16
+ meta = burndown["meta"]
17
+
18
+ total_days = meta["total_days"]
19
+
20
+ current_day = 1
21
+ x_days = []
22
+ y_open_story_points = []
23
+ y_open_tasks = []
24
+ total_tasks = []
25
+ total_story_points = []
26
+ x_days_extra = []
27
+ x_day_extra_start = []
28
+ y_story_points_done_extra = [0]
29
+ y_tasks_done_extra = [0]
30
+
31
+ for day in burndown["days"]:
32
+ x_days.append(current_day)
33
+ y_open_story_points.append(day["story_points"]["open"])
34
+ y_open_tasks.append(day["tasks"]["open"])
35
+ total_tasks.append(day["tasks"]["total"])
36
+ total_story_points.append(day["story_points"]["total"])
37
+
38
+ if "story_points_extra" in day or "tasks_extra" in day:
39
+ x_days_extra.append(current_day)
40
+ tasks = 0
41
+ if day.has_key("tasks_extra"):
42
+ tasks = -day["tasks_extra"]["done"]
43
+ y_tasks_done_extra.append(tasks)
44
+ points = 0
45
+ if day.has_key("story_points_extra"):
46
+ points = -day["story_points_extra"]["done"]
47
+ y_story_points_done_extra.append(points)
48
+
49
+ current_day += 1
50
+
51
+ # Add a day at the beginning of the extra days, so the curve starts at zero
52
+ if x_days_extra:
53
+ x_days_extra = [x_days_extra[0] - 1] + x_days_extra
54
+
55
+ scalefactor = float(total_tasks[0]) / float(y_open_story_points[0])
56
+
57
+ # Calculate minimum and maximum 'y' values for the axis
58
+ ymin_t_extra = 0
59
+ ymin_s_extra = 0
60
+ ymax = y_open_story_points[0] + 3
61
+
62
+ if len(y_tasks_done_extra) > 0:
63
+ ymin_t_extra = y_tasks_done_extra[len(y_tasks_done_extra) -1] -3
64
+ if len(y_story_points_done_extra) > 0:
65
+ ymin_s_extra = y_story_points_done_extra[len(y_story_points_done_extra) -1] -3
66
+ if ymin_t_extra < ymin_s_extra:
67
+ ymin = ymin_t_extra
68
+ else:
69
+ ymin = ymin_s_extra
70
+ if ymin_t_extra == 0 and ymin_s_extra == 0:
71
+ ymin = -3
72
+
73
+ # Plot in xkcd style
74
+ plt.xkcd()
75
+
76
+ plt.figure(1, figsize=(11, 6))
77
+
78
+ # Title of the burndown chart
79
+ plt.suptitle('Sprint ' + sprint, fontsize='large')
80
+
81
+ plt.xlabel('Days')
82
+ plt.axis([0, total_days + 1, ymin, ymax])
83
+ plt.plot([1, total_days] , [y_open_story_points[0], 0], color='grey')
84
+ plt.plot([0, total_days + 1], [0, 0], color='blue', linestyle=':')
85
+
86
+ # Weekend lines
87
+ for weekend_line in meta["weekend_lines"]:
88
+ plt.plot([weekend_line, weekend_line], [ymin+1, ymax-1], color='grey', linestyle=':')
89
+
90
+ # Story points
91
+ plt.ylabel('Story Points', color='black')
92
+ plt.plot(x_days, y_open_story_points, 'ko-', linewidth=2)
93
+ if x_days_extra:
94
+ plt.plot(x_days_extra, y_story_points_done_extra, 'ko-', linewidth=2)
95
+
96
+ # Tasks
97
+ plt.twinx()
98
+ plt.ylabel('Tasks', color='green')
99
+ plt.tick_params(axis='y', colors='green')
100
+ plt.axis([0, total_days + 1, ymin*scalefactor, ymax * scalefactor])
101
+ plt.plot(x_days, y_open_tasks, 'go-', linewidth=2)
102
+ if x_days_extra:
103
+ plt.plot(x_days_extra, y_tasks_done_extra, 'go-', linewidth=2)
104
+
105
+ # Calculation of new tasks
106
+ if len(total_tasks) > 1:
107
+ new_tasks = [0]
108
+ for i in range(1, len(total_tasks)):
109
+ new_tasks.append(total_tasks[i] - total_tasks[i - 1])
110
+ effective_new_tasks_days = []
111
+ effective_new_tasks = []
112
+ for i in range(len(new_tasks)):
113
+ if new_tasks[i] != 0:
114
+ effective_new_tasks_days.append(i - 0.25 + 1)
115
+ effective_new_tasks.append(new_tasks[i])
116
+ if len(effective_new_tasks) > 0:
117
+ plt.bar(effective_new_tasks_days, effective_new_tasks, .2, color='green')
118
+
119
+ # Calculation of new story points
120
+ if len(total_story_points) > 1:
121
+ new_story_points = [0]
122
+ for i in range(1, len(total_story_points)):
123
+ new_story_points.append(total_story_points[i] - total_story_points[i - 1])
124
+ effective_new_story_points_days = []
125
+ effective_new_story_points = []
126
+ for i in range(len(new_story_points)):
127
+ if new_story_points[i] != 0:
128
+ effective_new_story_points_days.append(i + 0.05 + 1)
129
+ effective_new_story_points.append(new_story_points[i])
130
+ if len(effective_new_story_points) > 0:
131
+ plt.bar(effective_new_story_points_days, effective_new_story_points, .2, color='black')
132
+
133
+ # Draw arrow showing already done tasks at begin of sprint
134
+ tasks_done = burndown["days"][0]["tasks"]["total"] - burndown["days"][0]["tasks"]["open"]
135
+
136
+ if tasks_done > 5:
137
+ plt.annotate("",
138
+ xy=(x_days[0], scalefactor * y_open_story_points[0] - 0.5 ), xycoords='data',
139
+ xytext=(x_days[0], y_open_tasks[0] + 0.5), textcoords='data',
140
+ arrowprops=dict(arrowstyle="<|-|>", connectionstyle="arc3", color='green')
141
+ )
142
+
143
+ plt.text(0.7, y_open_story_points[0], str(tasks_done) + " tasks done",
144
+ rotation='vertical', verticalalignment='center', color='green'
145
+ )
146
+
147
+ # Save the burndown chart
148
+ plt.savefig('burndown-' + sprint + '.png',bbox_inches='tight')
149
+ plt.show()
@@ -0,0 +1,307 @@
1
+ require_relative 'spec_helper'
2
+
3
+ include GivenFilesystemSpecHelpers
4
+
5
+ describe BurndownChart do
6
+
7
+ before(:each) do
8
+ @settings = dummy_settings
9
+ @burndown_data = BurndownData.new(@settings)
10
+ @chart = BurndownChart.new(@settings)
11
+ end
12
+
13
+ describe "initializer" do
14
+ it "sets initial meta data" do
15
+ expect(@chart.data["meta"]["sprint"]).to eq 1
16
+ expect(@chart.data["meta"]["total_days"]).to eq 10
17
+ expect(@chart.data["meta"]["weekend_lines"]).to eq [3.5, 8.5]
18
+ end
19
+ end
20
+
21
+ describe "data" do
22
+ use_given_filesystem
23
+
24
+ before(:each) do
25
+ @raw_data = [
26
+ {
27
+ "date" => '2014-04-23',
28
+ "story_points" =>
29
+ {
30
+ "total" => 30,
31
+ "open" => 23
32
+ },
33
+ "tasks" =>
34
+ {
35
+ "total" => 25,
36
+ "open" => 21
37
+ }
38
+ },
39
+ {
40
+ "date" => '2014-04-24',
41
+ "story_points" =>
42
+ {
43
+ "total" => 30,
44
+ "open" => 21
45
+ },
46
+ "tasks" =>
47
+ {
48
+ "total" => 26,
49
+ "open" => 19
50
+ },
51
+ "story_points_extra" =>
52
+ {
53
+ "done" => 3
54
+ },
55
+ "tasks_extra" =>
56
+ {
57
+ "done" => 2
58
+ }
59
+ }
60
+ ]
61
+ end
62
+
63
+ it "creates first data entry" do
64
+ @burndown_data.story_points.open = 16
65
+ @burndown_data.story_points.done = 7
66
+ @burndown_data.tasks.open = 10
67
+ @burndown_data.tasks.done = 11
68
+
69
+ @chart.add_data(@burndown_data,Date.parse("2014-05-30"))
70
+
71
+ expect( @chart.data["days"].first["story_points"] ).to eq(
72
+ {
73
+ "total" => 23,
74
+ "open" => 16
75
+ } )
76
+ expect( @chart.data["days"].first["tasks"] ).to eq(
77
+ {
78
+ "total" => 21,
79
+ "open" => 10
80
+ } )
81
+ end
82
+
83
+ it "returns sprint number" do
84
+ expect(@chart.sprint).to eq 1
85
+ end
86
+
87
+ it "adds data" do
88
+ @chart.data["days"] = @raw_data
89
+
90
+ @burndown_data.story_points.open = 16
91
+ @burndown_data.story_points.done = 7
92
+ @burndown_data.tasks.open = 10
93
+ @burndown_data.tasks.done = 11
94
+ @burndown_data.extra_story_points.open = 2
95
+ @burndown_data.extra_story_points.done = 3
96
+ @burndown_data.extra_tasks.open = 5
97
+ @burndown_data.extra_tasks.done = 2
98
+
99
+ @chart.add_data(@burndown_data,Date.parse("2014-05-30"))
100
+
101
+ expect( @chart.data["days"].count ).to eq 3
102
+ expect( @chart.data["days"].last["date"] ).to eq ( "2014-05-30" )
103
+ expect( @chart.data["days"].last["story_points"] ).to eq ( {
104
+ "total" => 23,
105
+ "open" => 16
106
+ } )
107
+ expect( @chart.data["days"].last["tasks"] ).to eq ( {
108
+ "total" => 21,
109
+ "open" => 10
110
+ } )
111
+ expect( @chart.data["days"].last["story_points_extra"] ).to eq ( {
112
+ "done" => 3
113
+ } )
114
+ expect( @chart.data["days"].last["tasks_extra"] ).to eq ( {
115
+ "done" => 2
116
+ } )
117
+ end
118
+
119
+ it "replaces data of same day" do
120
+ @chart.data["days"] = @raw_data
121
+
122
+ @burndown_data.story_points.open = 16
123
+ @burndown_data.story_points.done = 7
124
+ @burndown_data.tasks.open = 10
125
+ @burndown_data.tasks.done = 11
126
+
127
+ @chart.add_data(@burndown_data,Date.parse("2014-05-30"))
128
+
129
+ expect( @chart.data["days"].count ).to eq 3
130
+ expect( @chart.data["days"].last["story_points"] ).to eq ( {
131
+ "total" => 23,
132
+ "open" => 16
133
+ } )
134
+
135
+ @burndown_data.story_points.done = 8
136
+ @chart.add_data(@burndown_data,Date.parse("2014-05-30"))
137
+
138
+ expect( @chart.data["days"].count ).to eq 3
139
+ expect( @chart.data["days"].last["story_points"] ).to eq ( {
140
+ "total" => 24,
141
+ "open" => 16
142
+ } )
143
+ end
144
+
145
+ it "reads data" do
146
+ @chart.read_data given_file('burndown-data.yaml')
147
+
148
+ expect(@chart.data["days"]).to eq @raw_data
149
+ end
150
+
151
+ it "writes data" do
152
+ read_path = given_file('burndown-data.yaml')
153
+ @chart.read_data(read_path)
154
+
155
+ write_path = given_dummy_file
156
+ @chart.write_data(write_path)
157
+
158
+ expect(File.read(write_path)).to eq File.read(read_path)
159
+ end
160
+
161
+ it "doesn't write extra entries with 0 values" do
162
+ raw_data = [
163
+ {
164
+ "date" => '2014-04-24',
165
+ "story_points" =>
166
+ {
167
+ "total" => 30,
168
+ "open" => 21
169
+ },
170
+ "tasks" =>
171
+ {
172
+ "total" => 26,
173
+ "open" => 19
174
+ },
175
+ "story_points_extra" =>
176
+ {
177
+ "done" => 0
178
+ },
179
+ "tasks_extra" =>
180
+ {
181
+ "done" => 0
182
+ }
183
+ }
184
+ ]
185
+ @chart.data["days"] = raw_data
186
+ @chart.data["meta"]["board_id"] = "1234"
187
+
188
+ write_path = given_dummy_file
189
+ @chart.write_data(write_path)
190
+
191
+ expected_file_content = <<EOT
192
+ ---
193
+ meta:
194
+ board_id: '1234'
195
+ sprint: 1
196
+ total_days: 10
197
+ weekend_lines:
198
+ - 3.5
199
+ - 8.5
200
+ days:
201
+ - date: '2014-04-24'
202
+ story_points:
203
+ total: 30
204
+ open: 21
205
+ tasks:
206
+ total: 26
207
+ open: 19
208
+ EOT
209
+ expect(File.read(write_path)).to eq expected_file_content
210
+ end
211
+
212
+ end
213
+
214
+ describe "commands" do
215
+ use_given_filesystem(keep_files: true)
216
+
217
+ describe "setup" do
218
+ it "initializes new chart" do
219
+ path = given_directory
220
+ @chart.setup(path,"myboardid")
221
+
222
+ expect(File.exist?(File.join(path,"burndown-data-01.yaml"))).to be true
223
+
224
+ chart = BurndownChart.new(@settings)
225
+ chart.read_data(File.join(path,"burndown-data-01.yaml"))
226
+
227
+ expect(chart.board_id).to eq "myboardid"
228
+ end
229
+ end
230
+
231
+ describe "update" do
232
+ it "updates chart with latest data" do
233
+ card_url_match = /https:\/\/trello.com\/1\/boards\/myboardid\/cards\?-*/
234
+
235
+ stub_request(:any,card_url_match).to_return(:status => 200,
236
+ :body => load_test_file("cards.json"), :headers => {})
237
+
238
+ list_url_match = /https:\/\/trello.com\/1\/boards\/myboardid\/lists\?-*/
239
+
240
+ stub_request(:any,list_url_match).to_return(:status => 200,
241
+ :body => load_test_file("lists.json"), :headers => {})
242
+
243
+ path = given_directory_from_data("burndown_dir")
244
+
245
+ before = BurndownChart.new(@settings)
246
+ before.read_data(File.join(path,'burndown-data-02.yaml'))
247
+
248
+ @chart.update(path)
249
+
250
+ after = BurndownChart.new(@settings)
251
+ after.read_data(File.join(path,'burndown-data-02.yaml'))
252
+
253
+ expect(after.days.size).to eq before.days.size + 1
254
+ end
255
+
256
+ it "overwrites data on same date" do
257
+ card_url_match = /https:\/\/trello.com\/1\/boards\/myboardid\/cards\?-*/
258
+
259
+ stub_request(:any,card_url_match).to_return(:status => 200,
260
+ :body => load_test_file("cards.json"), :headers => {})
261
+
262
+ list_url_match = /https:\/\/trello.com\/1\/boards\/myboardid\/lists\?-*/
263
+
264
+ stub_request(:any,list_url_match).to_return(:status => 200,
265
+ :body => load_test_file("lists.json"), :headers => {})
266
+
267
+ path = given_directory_from_data("burndown_dir")
268
+
269
+ before = BurndownChart.new(@settings)
270
+ before.read_data(File.join(path,'burndown-data-02.yaml'))
271
+
272
+ @chart.update(path)
273
+ @chart.update(path)
274
+
275
+ after = BurndownChart.new(@settings)
276
+ after.read_data(File.join(path,'burndown-data-02.yaml'))
277
+
278
+ expect(after.days.size).to eq before.days.size + 1
279
+ end
280
+ end
281
+
282
+ describe "create_next_sprint" do
283
+ it "create new sprint file" do
284
+ path = given_directory_from_data("burndown_dir")
285
+ chart = BurndownChart.new(@settings)
286
+ chart.create_next_sprint(path)
287
+
288
+ next_sprint_file = File.join(path, "burndown-data-03.yaml")
289
+ expect(File.exist?(next_sprint_file)).to be true
290
+
291
+ expected_file_content = <<EOT
292
+ ---
293
+ meta:
294
+ board_id: myboardid
295
+ sprint: 3
296
+ total_days: 9
297
+ weekend_lines:
298
+ - 3.5
299
+ - 7.5
300
+ days: []
301
+ EOT
302
+ expect(File.read(next_sprint_file)).to eq expected_file_content
303
+ end
304
+ end
305
+ end
306
+
307
+ end