trollolo 0.0.3 → 0.0.4
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 +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +5 -1
- data/CHANGELOG.md +29 -0
- data/Gemfile +7 -2
- data/README.md +19 -0
- data/bin/trollolo +1 -1
- data/lib/array.rb +6 -0
- data/lib/backup.rb +67 -0
- data/lib/burndown_chart.rb +96 -67
- data/lib/burndown_data.rb +62 -123
- data/lib/card.rb +74 -30
- data/lib/cli.rb +131 -9
- data/lib/column.rb +61 -0
- data/lib/result.rb +0 -0
- data/lib/scrum_board.rb +104 -0
- data/lib/settings.rb +9 -4
- data/lib/trello_wrapper.rb +62 -0
- data/lib/trollolo.rb +10 -7
- data/lib/version.rb +1 -1
- data/scripts/.gitignore +1 -0
- data/scripts/burndowndata.py +113 -0
- data/scripts/create_burndown.py +111 -146
- data/scripts/graph.py +116 -0
- data/scripts/plot.py +131 -0
- data/spec/data/board.json +63 -0
- data/spec/data/burndown-data.yaml +3 -0
- data/spec/data/burndown_dir/burndown-data-01.yaml +1 -1
- data/spec/data/burndown_dir/burndown-data-02.yaml +1 -1
- data/spec/data/card.json +61 -0
- data/spec/data/full-board.json +1626 -0
- data/spec/data/lists.json +25 -25
- data/spec/data/trollolorc +5 -0
- data/spec/{command_line_spec.rb → integration/command_line_spec.rb} +1 -4
- data/spec/integration/create_burndown_spec.rb +57 -0
- data/spec/integration/integration_spec_helper.rb +10 -0
- data/spec/integration/support/aruba_hook.rb +11 -0
- data/spec/integration/support/custom_matchers.rb +13 -0
- data/spec/{wrapper → integration/wrapper}/credentials_input_wrapper +2 -2
- data/spec/{wrapper → integration/wrapper}/empty_config_trollolo_wrapper +2 -2
- data/spec/integration/wrapper/trollolo_wrapper +10 -0
- data/spec/unit/backup_spec.rb +107 -0
- data/spec/unit/burndown_chart_spec.rb +396 -0
- data/spec/unit/burndown_data_spec.rb +118 -0
- data/spec/unit/card_spec.rb +79 -0
- data/spec/unit/cli_spec.rb +38 -0
- data/spec/unit/retrieve_data_spec.rb +54 -0
- data/spec/unit/scrum_board_spec.rb +18 -0
- data/spec/{settings_spec.rb → unit/settings_spec.rb} +1 -1
- data/spec/{spec_helper.rb → unit/spec_helper.rb} +4 -12
- data/spec/unit/support/test_data_operations.rb +7 -0
- data/spec/unit/support/update_webmock_data +17 -0
- data/spec/unit/support/webmocks.rb +52 -0
- data/spec/unit/trello_wrapper_spec.rb +47 -0
- data/trollolo.gemspec +10 -11
- metadata +54 -37
- data/lib/trello.rb +0 -66
- data/spec/burndown_chart_spec.rb +0 -307
- data/spec/burndown_data_spec.rb +0 -125
- data/spec/card_spec.rb +0 -15
- data/spec/cli_spec.rb +0 -18
- data/spec/data/cards.json +0 -1002
- data/spec/trello_spec.rb +0 -32
- data/spec/wrapper/trollolo_wrapper +0 -11
data/lib/card.rb
CHANGED
@@ -16,49 +16,93 @@
|
|
16
16
|
# you may find current contact information at www.suse.com
|
17
17
|
|
18
18
|
class Card
|
19
|
+
# Assuming we have card titles as follows '(8) This is the card name'
|
20
|
+
ESTIMATED_REGEX = /\(([\d.]+)\)/
|
21
|
+
SPRINT_NUMBER_REGEX = /\ASprint (\d+)/
|
19
22
|
|
20
|
-
|
23
|
+
def initialize(board_data, card_id)
|
24
|
+
init_data(board_data, card_id)
|
25
|
+
end
|
21
26
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
$1.to_f
|
27
|
+
def init_data(board_data, card_id)
|
28
|
+
@board_data = board_data
|
29
|
+
@card_data = @board_data["cards"].select{|c| c["id"] == card_id}.first
|
26
30
|
end
|
27
31
|
|
28
|
-
def
|
29
|
-
|
30
|
-
@extra = false
|
32
|
+
def estimated?
|
33
|
+
name =~ ESTIMATED_REGEX
|
31
34
|
end
|
32
|
-
|
33
|
-
def
|
34
|
-
|
35
|
+
|
36
|
+
def story_points
|
37
|
+
return 0.0 unless estimated?
|
38
|
+
name.match(ESTIMATED_REGEX).captures.first.to_f
|
35
39
|
end
|
36
|
-
|
37
|
-
def
|
38
|
-
|
40
|
+
|
41
|
+
def done_tasks
|
42
|
+
count = 0
|
43
|
+
@card_data["checklists"].each do |checklist|
|
44
|
+
if checklist["name"] != "Feedback"
|
45
|
+
checklist["checkItems"].each do |checklist_item|
|
46
|
+
if checklist_item["state"] == "complete"
|
47
|
+
count += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
count
|
39
53
|
end
|
40
|
-
|
41
|
-
def
|
42
|
-
|
54
|
+
|
55
|
+
def tasks
|
56
|
+
count = 0
|
57
|
+
@card_data["checklists"].each do |checklist|
|
58
|
+
if checklist["name"] != "Feedback"
|
59
|
+
count += checklist["checkItems"].count
|
60
|
+
end
|
61
|
+
end
|
62
|
+
count
|
43
63
|
end
|
44
|
-
|
45
|
-
def self.parse json
|
46
|
-
card = Card.new
|
47
64
|
|
48
|
-
|
49
|
-
|
65
|
+
def card_labels
|
66
|
+
@card_data["labels"]
|
67
|
+
end
|
50
68
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
69
|
+
def desc
|
70
|
+
@card_data["desc"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def extra?
|
74
|
+
self.card_labels.any? do |label|
|
75
|
+
label['name'].include?('BelowWaterline') ||
|
76
|
+
label['name'].include?('Under waterline')
|
56
77
|
end
|
78
|
+
end
|
57
79
|
|
58
|
-
|
59
|
-
|
80
|
+
def meta_card?
|
81
|
+
name =~ SPRINT_NUMBER_REGEX
|
82
|
+
end
|
60
83
|
|
61
|
-
|
84
|
+
def sprint_number
|
85
|
+
raise ArgumentError unless meta_card?
|
86
|
+
name.match(SPRINT_NUMBER_REGEX).captures.first.to_i
|
62
87
|
end
|
63
88
|
|
89
|
+
def fast_lane?
|
90
|
+
# TODO: move to settings
|
91
|
+
self.card_labels.map{|l| l['name']}.include?('FastLane')
|
92
|
+
end
|
93
|
+
|
94
|
+
#TODO: rethink storage for meta data for sprint
|
95
|
+
def self.parse_yaml_from_description(description)
|
96
|
+
description =~ /```(yaml)?\n(.*)```/m
|
97
|
+
yaml = $2
|
98
|
+
if yaml
|
99
|
+
return YAML.load(yaml) # throws an exception for invalid yaml
|
100
|
+
else
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def name
|
106
|
+
@card_data["name"]
|
107
|
+
end
|
64
108
|
end
|
data/lib/cli.rb
CHANGED
@@ -37,6 +37,36 @@ class Cli < Thor
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
desc "get-raw [URL-FRAGMENT]", "Get raw JSON from Trello API"
|
41
|
+
long_desc <<EOT
|
42
|
+
Get raw JSON from Trello using the given URL fragment. Trollolo adds the server
|
43
|
+
part and API version as well as the credentials from the Trollolo configuration.
|
44
|
+
|
45
|
+
As example, the command
|
46
|
+
|
47
|
+
trollolo get-raw lists/53186e8391ef8671265eba9f/cards?filter=open
|
48
|
+
|
49
|
+
evaluates to the access of
|
50
|
+
|
51
|
+
https://api.trello.com/1/lists/53186e8391ef8671265eba9f/cards?filter=open&key=xxx&token=yyy
|
52
|
+
EOT
|
53
|
+
def get_raw(url_fragment)
|
54
|
+
process_global_options options
|
55
|
+
require_trello_credentials
|
56
|
+
|
57
|
+
url = "https://api.trello.com/1/#{url_fragment}"
|
58
|
+
if url_fragment =~ /\?/
|
59
|
+
url += "&"
|
60
|
+
else
|
61
|
+
url += "?"
|
62
|
+
end
|
63
|
+
url += "key=#{@@settings.developer_public_key}&token=#{@@settings.member_token}"
|
64
|
+
STDERR.puts "Calling #{url}"
|
65
|
+
|
66
|
+
response = Net::HTTP.get_response(URI.parse(url))
|
67
|
+
print JSON.pretty_generate(JSON.parse(response.body))
|
68
|
+
end
|
69
|
+
|
40
70
|
desc "get-lists", "Get lists"
|
41
71
|
option "board-id", :desc => "Id of Trello board", :required => true
|
42
72
|
def get_lists
|
@@ -162,22 +192,53 @@ class Cli < Thor
|
|
162
192
|
puts "Story points:"
|
163
193
|
puts " Open: #{burndown.story_points.open}"
|
164
194
|
puts " Done: #{burndown.story_points.done}"
|
165
|
-
puts "
|
195
|
+
puts " Total: #{burndown.story_points.total}"
|
196
|
+
puts
|
166
197
|
puts "Tasks:"
|
167
198
|
puts " Open: #{burndown.tasks.open}"
|
168
199
|
puts " Done: #{burndown.tasks.done}"
|
169
|
-
puts "
|
200
|
+
puts " Total: #{burndown.tasks.total}"
|
170
201
|
puts
|
171
202
|
puts "Extra story points:"
|
172
203
|
puts " Open: #{burndown.extra_story_points.open}"
|
173
204
|
puts " Done: #{burndown.extra_story_points.done}"
|
174
|
-
puts "
|
205
|
+
puts " Total: #{burndown.extra_story_points.total}"
|
175
206
|
puts "Extra tasks:"
|
176
207
|
puts " Open: #{burndown.extra_tasks.open}"
|
177
208
|
puts " Done: #{burndown.extra_tasks.done}"
|
178
|
-
puts "
|
209
|
+
puts " Total: #{burndown.extra_tasks.total}"
|
210
|
+
puts
|
211
|
+
puts "FastLane Cards:"
|
212
|
+
puts " Open: #{burndown.fast_lane_cards.open}"
|
213
|
+
puts " Done: #{burndown.fast_lane_cards.done}"
|
214
|
+
puts " Total: #{burndown.fast_lane_cards.total}"
|
179
215
|
end
|
180
216
|
|
217
|
+
desc "burndowns", "run multiple burndowns"
|
218
|
+
option "board-list", :desc => "path to board-list.yaml", :required => true
|
219
|
+
option :plot, :type => :boolean, :desc => "also plot the new data"
|
220
|
+
option :output, :aliases => :o, :desc => "Output directory"
|
221
|
+
def burndowns
|
222
|
+
process_global_options options
|
223
|
+
board_list = YAML.load_file(options["board-list"])
|
224
|
+
board_list.keys.each do |name|
|
225
|
+
if name =~ /[^[:alnum:]. _]/ # sanitize
|
226
|
+
raise "invalid character in team name"
|
227
|
+
end
|
228
|
+
board = board_list[name]
|
229
|
+
if options['output']
|
230
|
+
destdir = File.join(options['output'], name)
|
231
|
+
else
|
232
|
+
destdir = name
|
233
|
+
end
|
234
|
+
chart = BurndownChart.new @@settings
|
235
|
+
if ! File.directory?(destdir)
|
236
|
+
chart.setup(destdir, board["boardid"])
|
237
|
+
end
|
238
|
+
chart.update({'output' => destdir, plot: options[:plot]})
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
181
242
|
desc "burndown-init", "Initialize burndown chart"
|
182
243
|
option :output, :aliases => :o, :desc => "Output directory", :required => true
|
183
244
|
option "board-id", :desc => "Id of Trello board", :required => true
|
@@ -193,6 +254,9 @@ class Cli < Thor
|
|
193
254
|
desc "burndown", "Update burndown chart"
|
194
255
|
option :output, :aliases => :o, :desc => "Output directory", :required => false
|
195
256
|
option :new_sprint, :aliases => :n, :desc => "Create new sprint"
|
257
|
+
option :plot, :type => :boolean, :desc => "also plot the new data"
|
258
|
+
option 'with-fast-lane', :desc => "Plot Fast Lane with new cards bars", :required => false, :type => :boolean
|
259
|
+
option 'no-tasks', :desc => "Do not plot tasks line", :required => false, :type => :boolean
|
196
260
|
def burndown
|
197
261
|
process_global_options options
|
198
262
|
require_trello_credentials
|
@@ -202,19 +266,77 @@ class Cli < Thor
|
|
202
266
|
if options[:new_sprint]
|
203
267
|
chart.create_next_sprint(options[:output] || Dir.pwd)
|
204
268
|
end
|
205
|
-
chart.update(options
|
269
|
+
chart.update(options)
|
270
|
+
puts "Updated data for sprint #{chart.sprint}"
|
206
271
|
rescue TrolloloError => e
|
207
272
|
STDERR.puts e
|
208
273
|
exit 1
|
209
274
|
end
|
210
275
|
end
|
211
|
-
|
212
|
-
desc "plot", "Plot burndown chart"
|
276
|
+
|
277
|
+
desc "plot SPRINT-NUMBER [--output] [--no-tasks] [--with-fast-lane]", "Plot burndown chart for given sprint"
|
278
|
+
option :output, :aliases => :o, :desc => "Output directory", :required => false
|
279
|
+
option 'with-fast-lane', :desc => "Plot Fast Lane with new cards bars", :required => false, :type => :boolean
|
280
|
+
option 'no-tasks', :desc => "Do not plot tasks line", :required => false, :type => :boolean
|
213
281
|
def plot(sprint_number)
|
214
282
|
process_global_options options
|
283
|
+
BurndownChart.plot(sprint_number, options)
|
284
|
+
end
|
285
|
+
|
286
|
+
desc "backup", "Create backup of board"
|
287
|
+
option "board-id", :desc => "Id of Trello board", :required => true
|
288
|
+
def backup
|
289
|
+
process_global_options options
|
290
|
+
require_trello_credentials
|
215
291
|
|
216
|
-
|
217
|
-
|
292
|
+
b = Backup.new @@settings
|
293
|
+
b.backup(options["board-id"])
|
294
|
+
end
|
295
|
+
|
296
|
+
desc "list_backups", "List all backups"
|
297
|
+
def list_backups
|
298
|
+
b = Backup.new @@settings
|
299
|
+
b.list.each do |backup|
|
300
|
+
puts backup
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
desc "show_backup", "Show backup of board"
|
305
|
+
option "board-id", :desc => "Id of Trello board", :required => true
|
306
|
+
option "show-descriptions", :desc => "Show descriptions of cards", :required => false, :type => :boolean
|
307
|
+
def show_backup
|
308
|
+
b = Backup.new @@settings
|
309
|
+
b.show(options["board-id"], options)
|
310
|
+
end
|
311
|
+
|
312
|
+
desc "organization", "Show organization info"
|
313
|
+
option "org-name", :desc => "Name of organization", :required => true
|
314
|
+
def organization
|
315
|
+
process_global_options options
|
316
|
+
require_trello_credentials
|
317
|
+
|
318
|
+
trello = TrelloWrapper.new(@@settings)
|
319
|
+
|
320
|
+
o = trello.organization(options["org-name"])
|
321
|
+
|
322
|
+
puts "Display Name: #{o.display_name}"
|
323
|
+
puts "Home page: #{o.url}"
|
324
|
+
end
|
325
|
+
|
326
|
+
desc "organization_members", "Show organization members"
|
327
|
+
option "org-name", :desc => "Name of organization", :required => true
|
328
|
+
def organization_members
|
329
|
+
process_global_options options
|
330
|
+
require_trello_credentials
|
331
|
+
|
332
|
+
trello = TrelloWrapper.new(@@settings)
|
333
|
+
|
334
|
+
members = trello.organization(options["org-name"]).members
|
335
|
+
members.sort! { |a,b| a.username <=> b.username }
|
336
|
+
|
337
|
+
members.each do |member|
|
338
|
+
puts "#{member.username} (#{member.full_name})"
|
339
|
+
end
|
218
340
|
end
|
219
341
|
|
220
342
|
private
|
data/lib/column.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Copyright (c) 2013-2015 SUSE LLC
|
2
|
+
#
|
3
|
+
# This program is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of version 3 of the GNU General Public License as
|
5
|
+
# published by the Free Software Foundation.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program; if not, contact SUSE LLC.
|
14
|
+
#
|
15
|
+
# To contact SUSE about this file by physical or electronic mail,
|
16
|
+
# you may find current contact information at www.suse.com
|
17
|
+
class Column
|
18
|
+
def initialize(board_data, list_id)
|
19
|
+
@board_data = board_data
|
20
|
+
@list_data = @board_data["lists"].select{|l| l["id"] == list_id}.first
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
@list_data["name"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def estimated_cards
|
28
|
+
cards.select{|x| x.estimated? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def sum
|
32
|
+
estimated_cards.map{|x| x.story_points}.sum
|
33
|
+
end
|
34
|
+
|
35
|
+
def tasks
|
36
|
+
cards.map(&:tasks).sum
|
37
|
+
end
|
38
|
+
|
39
|
+
def done_tasks
|
40
|
+
cards.map(&:done_tasks).sum
|
41
|
+
end
|
42
|
+
|
43
|
+
def extra_cards
|
44
|
+
cards.select{|c| c.extra?}
|
45
|
+
end
|
46
|
+
|
47
|
+
def committed_cards
|
48
|
+
cards.select{|c| !c.extra?}
|
49
|
+
end
|
50
|
+
|
51
|
+
def fast_lane_cards
|
52
|
+
cards.select{|c| c.fast_lane?}
|
53
|
+
end
|
54
|
+
|
55
|
+
def cards
|
56
|
+
return @cards if @cards
|
57
|
+
|
58
|
+
cards = @board_data["cards"].select{|c| c["idList"] == @list_data["id"]}
|
59
|
+
@cards = cards.map{|c| Card.new(@board_data, c["id"])}
|
60
|
+
end
|
61
|
+
end
|
data/lib/result.rb
ADDED
File without changes
|
data/lib/scrum_board.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
class ScrumBoard
|
2
|
+
|
3
|
+
class DoneColumnNotFoundError < StandardError; end
|
4
|
+
|
5
|
+
def initialize(board_data, settings)
|
6
|
+
@settings = settings
|
7
|
+
@board_data = board_data
|
8
|
+
end
|
9
|
+
|
10
|
+
def columns
|
11
|
+
@columns ||= @board_data["lists"].map{|x| Column.new(@board_data, x["id"])}
|
12
|
+
end
|
13
|
+
|
14
|
+
def done_column
|
15
|
+
begin
|
16
|
+
done_columns = columns.select{|c| c.name =~ @settings.done_column_name_regex }
|
17
|
+
if done_columns.empty?
|
18
|
+
raise DoneColumnNotFoundError, "can't find done column by name regex #{@settings.done_column_name_regex}"
|
19
|
+
else
|
20
|
+
done_columns.max_by{|c| c.name.match(@settings.done_column_name_regex).captures.first.to_i }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def done_cards
|
26
|
+
done_column.committed_cards
|
27
|
+
end
|
28
|
+
|
29
|
+
def open_columns
|
30
|
+
columns.select{ |col| @settings.not_done_columns.include?(col.name) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def open_cards
|
34
|
+
open_columns.map{|col| col.committed_cards}.flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
def committed_cards
|
38
|
+
open_cards + done_cards
|
39
|
+
end
|
40
|
+
|
41
|
+
def done_story_points
|
42
|
+
done_cards.map(&:story_points).sum
|
43
|
+
end
|
44
|
+
|
45
|
+
def open_story_points
|
46
|
+
open_cards.map(&:story_points).sum
|
47
|
+
end
|
48
|
+
|
49
|
+
def closed_tasks
|
50
|
+
committed_cards.map(&:done_tasks).sum
|
51
|
+
end
|
52
|
+
|
53
|
+
def tasks
|
54
|
+
committed_cards.map(&:tasks).sum
|
55
|
+
end
|
56
|
+
|
57
|
+
def extra_cards
|
58
|
+
(done_column.extra_cards + open_columns.map(&:extra_cards)).flatten(1)
|
59
|
+
end
|
60
|
+
|
61
|
+
def extra_done_cards
|
62
|
+
done_column.extra_cards
|
63
|
+
end
|
64
|
+
|
65
|
+
def extra_done_story_points
|
66
|
+
extra_done_cards.map(&:story_points).sum
|
67
|
+
end
|
68
|
+
|
69
|
+
def extra_open_cards
|
70
|
+
open_columns.map{|col| col.cards.select{|c| c.extra?}}.flatten
|
71
|
+
end
|
72
|
+
|
73
|
+
def extra_open_story_points
|
74
|
+
extra_open_cards.map(&:story_points).sum
|
75
|
+
end
|
76
|
+
|
77
|
+
def extra_tasks
|
78
|
+
extra_cards.map(&:tasks).sum
|
79
|
+
end
|
80
|
+
|
81
|
+
def extra_closed_tasks
|
82
|
+
extra_cards.map(&:done_tasks).sum
|
83
|
+
end
|
84
|
+
|
85
|
+
def open_fast_lane_cards_count
|
86
|
+
open_columns.map(&:fast_lane_cards).flatten(1).count
|
87
|
+
end
|
88
|
+
|
89
|
+
def done_fast_lane_cards_count
|
90
|
+
done_column.fast_lane_cards.count
|
91
|
+
end
|
92
|
+
|
93
|
+
def scrum_cards
|
94
|
+
open_columns.map(&:fast_lane_cards).flatten(1) + done_column.cards
|
95
|
+
end
|
96
|
+
|
97
|
+
def meta_cards
|
98
|
+
scrum_cards.select{|c| c.meta_card? }
|
99
|
+
end
|
100
|
+
|
101
|
+
def id
|
102
|
+
@board_data["id"]
|
103
|
+
end
|
104
|
+
end
|