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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +5 -1
  4. data/CHANGELOG.md +29 -0
  5. data/Gemfile +7 -2
  6. data/README.md +19 -0
  7. data/bin/trollolo +1 -1
  8. data/lib/array.rb +6 -0
  9. data/lib/backup.rb +67 -0
  10. data/lib/burndown_chart.rb +96 -67
  11. data/lib/burndown_data.rb +62 -123
  12. data/lib/card.rb +74 -30
  13. data/lib/cli.rb +131 -9
  14. data/lib/column.rb +61 -0
  15. data/lib/result.rb +0 -0
  16. data/lib/scrum_board.rb +104 -0
  17. data/lib/settings.rb +9 -4
  18. data/lib/trello_wrapper.rb +62 -0
  19. data/lib/trollolo.rb +10 -7
  20. data/lib/version.rb +1 -1
  21. data/scripts/.gitignore +1 -0
  22. data/scripts/burndowndata.py +113 -0
  23. data/scripts/create_burndown.py +111 -146
  24. data/scripts/graph.py +116 -0
  25. data/scripts/plot.py +131 -0
  26. data/spec/data/board.json +63 -0
  27. data/spec/data/burndown-data.yaml +3 -0
  28. data/spec/data/burndown_dir/burndown-data-01.yaml +1 -1
  29. data/spec/data/burndown_dir/burndown-data-02.yaml +1 -1
  30. data/spec/data/card.json +61 -0
  31. data/spec/data/full-board.json +1626 -0
  32. data/spec/data/lists.json +25 -25
  33. data/spec/data/trollolorc +5 -0
  34. data/spec/{command_line_spec.rb → integration/command_line_spec.rb} +1 -4
  35. data/spec/integration/create_burndown_spec.rb +57 -0
  36. data/spec/integration/integration_spec_helper.rb +10 -0
  37. data/spec/integration/support/aruba_hook.rb +11 -0
  38. data/spec/integration/support/custom_matchers.rb +13 -0
  39. data/spec/{wrapper → integration/wrapper}/credentials_input_wrapper +2 -2
  40. data/spec/{wrapper → integration/wrapper}/empty_config_trollolo_wrapper +2 -2
  41. data/spec/integration/wrapper/trollolo_wrapper +10 -0
  42. data/spec/unit/backup_spec.rb +107 -0
  43. data/spec/unit/burndown_chart_spec.rb +396 -0
  44. data/spec/unit/burndown_data_spec.rb +118 -0
  45. data/spec/unit/card_spec.rb +79 -0
  46. data/spec/unit/cli_spec.rb +38 -0
  47. data/spec/unit/retrieve_data_spec.rb +54 -0
  48. data/spec/unit/scrum_board_spec.rb +18 -0
  49. data/spec/{settings_spec.rb → unit/settings_spec.rb} +1 -1
  50. data/spec/{spec_helper.rb → unit/spec_helper.rb} +4 -12
  51. data/spec/unit/support/test_data_operations.rb +7 -0
  52. data/spec/unit/support/update_webmock_data +17 -0
  53. data/spec/unit/support/webmocks.rb +52 -0
  54. data/spec/unit/trello_wrapper_spec.rb +47 -0
  55. data/trollolo.gemspec +10 -11
  56. metadata +54 -37
  57. data/lib/trello.rb +0 -66
  58. data/spec/burndown_chart_spec.rb +0 -307
  59. data/spec/burndown_data_spec.rb +0 -125
  60. data/spec/card_spec.rb +0 -15
  61. data/spec/cli_spec.rb +0 -18
  62. data/spec/data/cards.json +0 -1002
  63. data/spec/trello_spec.rb +0 -32
  64. data/spec/wrapper/trollolo_wrapper +0 -11
@@ -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
- attr_accessor :sp, :tasks, :tasks_done
23
+ def initialize(board_data, card_id)
24
+ init_data(board_data, card_id)
25
+ end
21
26
 
22
- def self.name_to_points(card_name)
23
- card_name =~ /^\(([\d.]+)\)/
24
- return nil if $1.nil?
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 initialize
29
- @sp = nil
30
- @extra = false
32
+ def estimated?
33
+ name =~ ESTIMATED_REGEX
31
34
  end
32
-
33
- def has_sp?
34
- @sp != nil
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 extra?
38
- @extra
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 set_extra
42
- @extra = true
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
- title = json["name"]
49
- card.sp = name_to_points(title)
65
+ def card_labels
66
+ @card_data["labels"]
67
+ end
50
68
 
51
- labels = json["labels"]
52
- labels.each do |label|
53
- if label["name"] == "Under waterline"
54
- card.set_extra
55
- end
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
- card.tasks = json["badges"]["checkItems"]
59
- card.tasks_done = json["badges"]["checkItemsChecked"]
80
+ def meta_card?
81
+ name =~ SPRINT_NUMBER_REGEX
82
+ end
60
83
 
61
- card
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 " Total: #{burndown.story_points.total}"
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 " Total: #{burndown.tasks.total}"
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 " Total: #{burndown.extra_story_points.total}"
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 " Total: #{burndown.extra_tasks.total}"
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[:output] || Dir.pwd)
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
- plot_helper = File.expand_path("../../scripts/create_burndown.py", __FILE__ )
217
- system "python #{plot_helper} #{sprint_number}"
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
@@ -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
File without changes
@@ -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