trollolo 0.0.3 → 0.0.4

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