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
@@ -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
+