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
@@ -17,8 +17,9 @@
17
17
 
18
18
  class Settings
19
19
 
20
- attr_accessor :verbose, :raw
21
- attr_accessor :developer_public_key, :member_token
20
+ attr_accessor :developer_public_key, :member_token, :verbose, :raw,
21
+ :not_done_columns, :todo_column, :done_column_name_regex,
22
+ :todo_column_name_regex
22
23
 
23
24
  def initialize config_file_path
24
25
  @config_file_path = config_file_path
@@ -26,8 +27,12 @@ class Settings
26
27
  @config = YAML.load_file(config_file_path)
27
28
 
28
29
  if @config
29
- @developer_public_key = @config["developer_public_key"]
30
- @member_token = @config["member_token"]
30
+ @developer_public_key = @config["developer_public_key"]
31
+ @member_token = @config["member_token"]
32
+ @not_done_columns = @config["not_done_columns"].freeze || ["Sprint Backlog", "Doing"]
33
+ @todo_column = @config["todo_column"].freeze
34
+ @done_column_name_regex = @config["done_column_name_regex"].freeze || /\ADone Sprint (\d+)\Z/
35
+ @todo_column_name_regex = @config["todo_column_name_regex"].freeze || /\ATo Do\Z/
31
36
  else
32
37
  raise "Couldn't read config data from '#{config_file_path}'"
33
38
  end
@@ -0,0 +1,62 @@
1
+ # Copyright (c) 2013-2014 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
+ require 'trello'
18
+
19
+ class TrelloWrapper
20
+
21
+ attr_accessor :board
22
+
23
+ def initialize(settings)
24
+ @settings = settings
25
+ init_trello
26
+ end
27
+
28
+ def client
29
+ Trello::Client.new(
30
+ developer_public_key: @settings.developer_public_key,
31
+ member_token: @settings.member_token
32
+ )
33
+ end
34
+
35
+ def board(board_id)
36
+ return @board if @board
37
+
38
+ @board = ScrumBoard.new(retrieve_board_data(board_id), @settings)
39
+ end
40
+
41
+ def retrieve_board_data(board_id)
42
+ JSON.parse(client.get("/boards/#{board_id}?lists=open&cards=open&card_checklists=all"))
43
+ end
44
+
45
+ def backup(board_id)
46
+ client.get("/boards/#{board_id}?lists=open&cards=open&card_checklists=all")
47
+ end
48
+
49
+ def organization(org_id)
50
+ Trello::Organization.find(org_id)
51
+ end
52
+
53
+ private
54
+
55
+ def init_trello
56
+ Trello.configure do |config|
57
+ config.developer_public_key = @settings.developer_public_key
58
+ config.member_token = @settings.member_token
59
+ end
60
+ end
61
+
62
+ end
@@ -15,20 +15,23 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
- require "thor"
19
- require "net/http"
20
- require "net/https"
21
- require "json"
22
- require "yaml"
23
- require "erb"
18
+ require 'thor'
19
+ require 'json'
20
+ require 'yaml'
21
+ require 'erb'
24
22
 
23
+ require_relative 'array'
25
24
  require_relative 'version'
26
25
  require_relative 'cli'
27
26
  require_relative 'settings'
28
- require_relative 'trello'
27
+ require_relative 'column'
29
28
  require_relative 'card'
29
+ require_relative 'scrum_board'
30
+ require_relative 'result'
31
+ require_relative 'trello_wrapper'
30
32
  require_relative 'burndown_chart'
31
33
  require_relative 'burndown_data'
34
+ require_relative 'backup'
32
35
 
33
36
  class TrolloloError < StandardError
34
37
  end
@@ -1,5 +1,5 @@
1
1
  module Trollolo
2
2
 
3
- VERSION = "0.0.3"
3
+ VERSION = "0.0.4"
4
4
 
5
5
  end
@@ -0,0 +1 @@
1
+ *.pyc
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python
2
+ import yaml
3
+
4
+
5
+ class BurndownData:
6
+ "Store burndown data parsed from YAML file"
7
+
8
+ def __init__(self, args):
9
+ self.args = args
10
+ burndown = self.readYAML(self.args.sprint)
11
+ self.getSprintData(burndown)
12
+ self.calculateMaxStoryPoints()
13
+ self.setBonusTasksDayOne(burndown)
14
+ self.setExtraDays()
15
+ self.calculateYRange(self.max_story_points, self.bonus_tasks_done, self.bonus_story_points_done)
16
+ self.setScaleFactor(self.total_tasks[0], self.max_story_points)
17
+
18
+ def readYAML(self, sprint_number):
19
+ with open('burndown-data-' + sprint_number + '.yaml', 'r') as f:
20
+ burndown = yaml.load(f)
21
+ return burndown
22
+
23
+ def getSprintData(self, burndown):
24
+ self.sprint_number = burndown["meta"]["sprint"]
25
+ self.weekend_lines = burndown["meta"]["weekend_lines"]
26
+ self.total_days = burndown["meta"]["total_days"]
27
+ self.extra_day = 0
28
+ self.current_day = 1
29
+ self.days = []
30
+ self.tasks_extra_days = []
31
+ self.story_points_extra_days = []
32
+ self.open_story_points = []
33
+ self.total_story_points = []
34
+ self.bonus_story_points_done = []
35
+ self.open_tasks = []
36
+ self.total_tasks = []
37
+ self.bonus_tasks_done = []
38
+ self.x_fast_lane = []
39
+ self.y_fast_lane = []
40
+ self.total_fast_lane = []
41
+ self.max_story_points = 0
42
+
43
+ for day in burndown["days"]:
44
+ self.days.append(self.current_day)
45
+ self.open_story_points.append(day["story_points"]["open"])
46
+ self.total_story_points.append(day["story_points"]["total"])
47
+ self.open_tasks.append(day["tasks"]["open"])
48
+ self.total_tasks.append(day["tasks"]["total"])
49
+
50
+ if "tasks_extra" in day:
51
+ self.tasks_extra_days.append(self.current_day)
52
+ tasks = -day["tasks_extra"]["done"]
53
+ self.bonus_tasks_done.append(tasks)
54
+
55
+ if "story_points_extra" in day:
56
+ self.story_points_extra_days.append(self.current_day)
57
+ points = -day["story_points_extra"]["done"]
58
+ self.bonus_story_points_done.append(points)
59
+
60
+ if day.has_key("fast_lane"):
61
+ self.x_fast_lane.append(self.current_day)
62
+ self.y_fast_lane.append(day["fast_lane"]["open"])
63
+ self.total_fast_lane.append(day["fast_lane"]["total"])
64
+
65
+ self.current_day += 1
66
+ return
67
+
68
+ def calculateMaxStoryPoints(self):
69
+ for sp in self.total_story_points:
70
+ self.max_story_points = max(self.max_story_points, sp)
71
+ return
72
+
73
+ def setBonusTasksDayOne(self, burndown):
74
+ if burndown["days"][0].has_key("tasks_extra"):
75
+ self.bonus_tasks_day_one = burndown["days"][0]["tasks_extra"]["done"]
76
+ else:
77
+ self.bonus_tasks_day_one = 0
78
+ return
79
+
80
+ def setExtraDays(self):
81
+ if len(self.story_points_extra_days) > 0:
82
+ self.story_points_extra_days = [self.story_points_extra_days[0] - 1] + self.story_points_extra_days
83
+ self.bonus_story_points_done = [0] + self.bonus_story_points_done
84
+ if len(self.tasks_extra_days) > 0:
85
+ if not self.args.no_tasks and not self.bonus_tasks_day_one:
86
+ self.tasks_extra_days = [self.tasks_extra_days[0] - 1] + self.tasks_extra_days
87
+ self.bonus_tasks_done = [0] + self.bonus_tasks_done
88
+ self.extra_day = 1
89
+ return
90
+
91
+ def calculateYRange(self, max_story_points, bonus_tasks_done, bonus_story_points_done):
92
+ self.ymax = max_story_points + 3
93
+
94
+ if len(bonus_tasks_done) > 0:
95
+ ymin_bonus_tasks = min(bonus_tasks_done) -3
96
+ else:
97
+ ymin_bonus_tasks = 0
98
+
99
+ ymin_bonus_story_points = 0
100
+
101
+ if len(bonus_story_points_done) > 0:
102
+ ymin_bonus_story_points = min(bonus_story_points_done) -3
103
+
104
+ if ymin_bonus_tasks == 0 and ymin_bonus_story_points == 0:
105
+ self.ymin = -3
106
+ else:
107
+ self.ymin = min(ymin_bonus_tasks, ymin_bonus_story_points)
108
+ return
109
+
110
+ def setScaleFactor(self, total_tasks, max_story_points):
111
+ self.scalefactor = float(total_tasks) / float(max_story_points)
112
+ return
113
+
@@ -1,149 +1,114 @@
1
- #!/usr/bin/python
1
+ #!/usr/bin/env python
2
+ import matplotlib
3
+ import imp
4
+ try:
5
+ imp.find_module('TkAgg')
6
+ mac_backend_available = True
7
+ except ImportError:
8
+ mac_backend_available = False
9
+
10
+ if mac_backend_available:
11
+ matplotlib.use('TkAgg')
12
+
2
13
  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
- )
14
+ import argparse
15
+ import os
16
+
17
+ import burndowndata
18
+ import plot
19
+ import graph
20
+
21
+
22
+ def parseCommandLine():
23
+ epilog = "Look at https://github.com/openSUSE/trollolo for details"
24
+ description = "Generates Scrum Burndown Chart from YAML file"
25
+
26
+ parser = argparse.ArgumentParser(epilog=epilog, description=description)
27
+ parser.add_argument('sprint', metavar='NUM', help='Sprint Number')
28
+ parser.add_argument('--output', help='Location of data to process')
29
+ parser.add_argument('--no-tasks', action='store_true', help='Disable Tasks line in the chart', default=False)
30
+ parser.add_argument('--with-fast-lane', action='store_true', help='Draw line for Fast Lane cards', default=False)
31
+ parser.add_argument('--verbose', action='store_true', help='Verbose Output', default=False)
32
+ parser.add_argument('--no-head', action='store_true', help='Run in headless mode', default=False)
33
+ args = parser.parse_args()
34
+
35
+ if args.output:
36
+ os.chdir(args.output)
37
+
38
+ if args.verbose:
39
+ print args
40
+
41
+ return args
42
+
43
+
44
+
45
+ ### MAIN ###
46
+
47
+ # parseCommandLine() needs to be called at the beginning to retrieve the
48
+ # command line parameters or provide help on these. It returns a dict
49
+ # containing the state of all parameters. Currently the following
50
+ # parameters are available:
51
+ #
52
+ # Mandatory parameters:
53
+ # NUM The sprint number for which the burndown chart should be generated
54
+ #
55
+ # Optional parameters:
56
+ # --file Specify the location of the YAML file containing the sprint data,
57
+ # if not provided, the YAML file is expected to be in the current
58
+ # working directory
59
+ #
60
+ # --no-tasks Disable drawing the tasks graph in the chart
61
+ #
62
+ # --with-fast-lane Enable drawing the graph for fast lane cards in the chart
63
+ #
64
+ # --no-head Disable showing the graph, to be used for automation
65
+ #
66
+ # --verbose Verbose output
67
+ #
68
+ args = parseCommandLine()
69
+
70
+ # Create burndown data object
71
+ data = burndowndata.BurndownData(args)
72
+
73
+ # Configure plot parameters
74
+ plot = plot.Plot(data)
75
+
76
+ title = "Sprint" + str(data.sprint_number)
77
+ title_fontsize = 'large'
78
+ plot.setTitle(title, title_fontsize)
79
+ plot.setXAxisLabel("Days")
80
+
81
+ plot.drawDiagonal("grey")
82
+ plot.drawWaterLine("blue", ":")
83
+ plot.drawWeekendLines("grey", ":")
84
+
85
+ # Plot all graphs
86
+ graph_story_points = graph.Graph(plot.storyPoints())
87
+
88
+ y_label = "Story Points"
89
+ color = "black"
90
+ marker = "o"
91
+ linestyle = "solid"
92
+ linewidth = 2
93
+
94
+ graph_story_points.draw(y_label, color, marker, linestyle, linewidth, plot)
95
+
96
+ if not args.no_tasks:
97
+ graph_tasks = graph.Graph(plot.tasks())
98
+
99
+ y_label = "Tasks"
100
+ color = "green"
101
+
102
+ graph_tasks.draw(y_label, color, marker, linestyle, linewidth, plot)
103
+
104
+ if args.with_fast_lane:
105
+ graph_fast_lane = graph.Graph(plot.fastLane())
106
+
107
+ y_label = "Fast Lane"
108
+ color = "red"
109
+
110
+ graph_fast_lane.draw(y_label, color, marker, linestyle, linewidth, plot)
146
111
 
147
112
  # Save the burndown chart
148
- plt.savefig('burndown-' + sprint + '.png',bbox_inches='tight')
149
- plt.show()
113
+ plot.saveImage(args)
114
+