trollolo 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|