trollolo 0.0.3

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,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