trollolo 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +17 -0
- data/CONTRIBUTING.md +7 -0
- data/COPYING +674 -0
- data/Gemfile +11 -0
- data/README.md +92 -0
- data/Rakefile +12 -0
- data/bin/trollolo +33 -0
- data/lib/burndown_chart.rb +158 -0
- data/lib/burndown_data.rb +172 -0
- data/lib/card.rb +64 -0
- data/lib/cli.rb +251 -0
- data/lib/settings.rb +54 -0
- data/lib/trello.rb +66 -0
- data/lib/trollolo.rb +34 -0
- data/lib/version.rb +5 -0
- data/man/.gitignore +4 -0
- data/man/trollolo.1.md +152 -0
- data/scripts/create_burndown.py +149 -0
- data/spec/burndown_chart_spec.rb +307 -0
- data/spec/burndown_data_spec.rb +125 -0
- data/spec/card_spec.rb +15 -0
- data/spec/cli_spec.rb +18 -0
- data/spec/command_line_spec.rb +56 -0
- data/spec/data/burndown-data.yaml +26 -0
- data/spec/data/burndown_dir/burndown-data-01.yaml +9 -0
- data/spec/data/burndown_dir/burndown-data-02.yaml +9 -0
- data/spec/data/burndown_dir/create_burndown +142 -0
- data/spec/data/cards.json +1002 -0
- data/spec/data/lists.json +50 -0
- data/spec/data/trollolorc +2 -0
- data/spec/settings_spec.rb +39 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/trello_spec.rb +32 -0
- data/spec/wrapper/credentials_input_wrapper +19 -0
- data/spec/wrapper/empty_config_trollolo_wrapper +10 -0
- data/spec/wrapper/trollolo_wrapper +11 -0
- data/trollolo.gemspec +28 -0
- metadata +131 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Trollolo
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/openSUSE/trollolo.svg?branch=master)](https://travis-ci.org/openSUSE/trollolo)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/openSUSE/trollolo/badges/gpa.svg)](https://codeclimate.com/github/openSUSE/trollolo)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/openSUSE/trollolo/badges/coverage.svg)](https://codeclimate.com/github/openSUSE/trollolo)
|
6
|
+
|
7
|
+
Command line tool to extract data from Trello, in particular for creating
|
8
|
+
burndown charts.
|
9
|
+
|
10
|
+
## Functionality
|
11
|
+
|
12
|
+
A detailed description of the functionality of the tool can be found in the
|
13
|
+
[man page](http://github.com/openSUSE/trollolo/blob/master/man/trollolo.1.md).
|
14
|
+
|
15
|
+
## Expectations
|
16
|
+
|
17
|
+
For expectations how the board has to be structured to make the burndown chart
|
18
|
+
functions work see the Trollolo man page. There is an
|
19
|
+
[example Trello board](https://trello.com/b/CRdddpdy/trollolo-testing-board)
|
20
|
+
which demonstrates the expected structure.
|
21
|
+
|
22
|
+
## Configuration
|
23
|
+
|
24
|
+
Trollolo reads a configuration file `.trollolorc` in the home directory of the
|
25
|
+
user running the command line tool. It reads the data required to authenticate
|
26
|
+
with the Trello server from it. It's two values (the example shows random data):
|
27
|
+
|
28
|
+
```yaml
|
29
|
+
developer_public_key: 87349873487ef8732487234
|
30
|
+
member_token: 87345897238957a29835789b2374580927f3589072398579820345
|
31
|
+
```
|
32
|
+
|
33
|
+
These values have to be set with the personal access data for the Trello API
|
34
|
+
and the id of the board, which is processed.
|
35
|
+
|
36
|
+
For creating a developer key go to the
|
37
|
+
[Developer API Keys](https://trello.com/1/appKey/generate) page on Trello. It's
|
38
|
+
the key in the first box.
|
39
|
+
|
40
|
+
For creating a member token go follow the
|
41
|
+
[instructions](https://trello.com/docs/gettingstarted/index.html#getting-a-token-from-a-user)
|
42
|
+
in the Trello API documentation.
|
43
|
+
|
44
|
+
The board id is the cryptic string in the URL of your board.
|
45
|
+
|
46
|
+
## Creating burndown charts
|
47
|
+
|
48
|
+
Trollolo implements a simple work flow for creating burndown charts from the
|
49
|
+
data on a Trello board. It fetches the data from Trello, stores and processes
|
50
|
+
it locally, and generates charts which can then be uploaded as graphics to
|
51
|
+
Trello again.
|
52
|
+
|
53
|
+
At the moment it only needs read-only access to the Trello board from which it
|
54
|
+
reads the data. In the future it would be great, if it could also write back
|
55
|
+
the generated data and results to make it even more automatic.
|
56
|
+
|
57
|
+
The work flow goes as follows:
|
58
|
+
|
59
|
+
Create an initial working directory for the burndown chart generation:
|
60
|
+
|
61
|
+
trollolo burndown-init --board-id=MYBOARDID --output=WORKING_DIR
|
62
|
+
|
63
|
+
This will create a directory WORKING_DIR and put an initial data file there,
|
64
|
+
which contains the meta data. The file is called `burndown-data-1.yaml`. You
|
65
|
+
might want to keep this file in a git repository for safe storage and history.
|
66
|
+
|
67
|
+
After each daily go to the working directory and call:
|
68
|
+
|
69
|
+
trollolo burndown
|
70
|
+
|
71
|
+
This will get the current data from the Trello board and update the data file
|
72
|
+
with the data from the current day. If there already was some data in the file
|
73
|
+
for the same day it will be overridden.
|
74
|
+
|
75
|
+
When the sprint is over and you want to start with the next sprint, go to the
|
76
|
+
working directory and call:
|
77
|
+
|
78
|
+
trollolo burndown --new-sprint
|
79
|
+
|
80
|
+
This will create a new data file for the next sprint number and populate it
|
81
|
+
with initial data taken from the Trello board. You are ready to go for the
|
82
|
+
sprint now and can continue with calling `trollolo burndown` after each daily.
|
83
|
+
|
84
|
+
To generate the actual burndown chart, go to the working directory and call:
|
85
|
+
|
86
|
+
trollolo plot SPRINT_NUMBER
|
87
|
+
|
88
|
+
This will take the data from the file `burndown-data-SPRINT_NUMBER.yaml` and
|
89
|
+
create a nice chart from it. It will show the chart and also create a file
|
90
|
+
`burndown-SPRINT_NUMBER.png` you can upload as cover graphics to a card on your
|
91
|
+
Trello board.
|
92
|
+
|
data/Rakefile
ADDED
data/bin/trollolo
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Copyright (c) 2013-2014 SUSE LLC
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of version 3 of the GNU General Public License as
|
6
|
+
# published by the Free Software Foundation.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program; if not, contact SUSE LLC.
|
15
|
+
#
|
16
|
+
# To contact SUSE about this file by physical or electronic mail,
|
17
|
+
# you may find current contact information at www.suse.com
|
18
|
+
|
19
|
+
require_relative '../lib/trollolo'
|
20
|
+
|
21
|
+
config_path = ENV["TROLLOLO_CONFIG_PATH"] || File.expand_path("~/.trollolorc")
|
22
|
+
|
23
|
+
Cli.settings = Settings.new(config_path)
|
24
|
+
|
25
|
+
# Set debug flag, so thor throws exceptions on error
|
26
|
+
ENV["THOR_DEBUG"] = "1"
|
27
|
+
begin
|
28
|
+
Cli.check_unknown_options!
|
29
|
+
result = Cli.start ARGV
|
30
|
+
rescue Thor::Error => e
|
31
|
+
STDERR.puts e
|
32
|
+
exit 1
|
33
|
+
end
|
@@ -0,0 +1,158 @@
|
|
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
|
+
|
18
|
+
class BurndownChart
|
19
|
+
|
20
|
+
attr_accessor :data
|
21
|
+
|
22
|
+
def initialize(settings)
|
23
|
+
@settings = settings
|
24
|
+
@burndown_data = BurndownData.new settings
|
25
|
+
|
26
|
+
@data = {
|
27
|
+
"meta" => {
|
28
|
+
"board_id" => nil,
|
29
|
+
"sprint" => 1,
|
30
|
+
"total_days" => 10,
|
31
|
+
"weekend_lines" => [ 3.5, 8.5 ]
|
32
|
+
},
|
33
|
+
"days" => []
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def sprint
|
38
|
+
@data["meta"]["sprint"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def sprint= s
|
42
|
+
@data["meta"]["sprint"] = s
|
43
|
+
end
|
44
|
+
|
45
|
+
def board_id
|
46
|
+
@data["meta"]["board_id"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def board_id= id
|
50
|
+
@data["meta"]["board_id"] = id
|
51
|
+
end
|
52
|
+
|
53
|
+
def days
|
54
|
+
@data["days"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_data(burndown_data, date)
|
58
|
+
new_entry = {
|
59
|
+
"date" => date.to_s,
|
60
|
+
"story_points" => {
|
61
|
+
"total" => burndown_data.story_points.total,
|
62
|
+
"open" => burndown_data.story_points.open
|
63
|
+
},
|
64
|
+
"tasks" => {
|
65
|
+
"total" => burndown_data.tasks.total,
|
66
|
+
"open" => burndown_data.tasks.open
|
67
|
+
},
|
68
|
+
"story_points_extra" => {
|
69
|
+
"done" => burndown_data.extra_story_points.done
|
70
|
+
},
|
71
|
+
"tasks_extra" => {
|
72
|
+
"done" => burndown_data.extra_tasks.done
|
73
|
+
}
|
74
|
+
}
|
75
|
+
new_days = Array.new
|
76
|
+
replaced_entry = false
|
77
|
+
@data["days"].each do |entry|
|
78
|
+
if entry["date"] == date.to_s
|
79
|
+
new_days.push(new_entry)
|
80
|
+
replaced_entry = true
|
81
|
+
else
|
82
|
+
new_days.push(entry)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
if !replaced_entry
|
86
|
+
new_days.push(new_entry)
|
87
|
+
end
|
88
|
+
@data["days"] = new_days
|
89
|
+
end
|
90
|
+
|
91
|
+
def read_data filename
|
92
|
+
@data = YAML.load_file filename
|
93
|
+
end
|
94
|
+
|
95
|
+
def write_data filename
|
96
|
+
@data["days"].each do |day|
|
97
|
+
[ "story_points_extra", "tasks_extra" ].each do |key|
|
98
|
+
if day[key] && day[key]["done"] == 0
|
99
|
+
day.delete key
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
File.open( filename, "w" ) do |file|
|
105
|
+
file.write @data.to_yaml
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def burndown_data_filename
|
110
|
+
"burndown-data-#{sprint.to_s.rjust(2,"0")}.yaml"
|
111
|
+
end
|
112
|
+
|
113
|
+
def setup(burndown_dir, board_id)
|
114
|
+
self.board_id = board_id
|
115
|
+
FileUtils.mkdir_p burndown_dir
|
116
|
+
write_data File.join(burndown_dir, burndown_data_filename)
|
117
|
+
end
|
118
|
+
|
119
|
+
def update(burndown_dir)
|
120
|
+
Dir.glob("#{burndown_dir}/burndown-data-*.yaml").each do |file|
|
121
|
+
file =~ /burndown-data-(.*).yaml/
|
122
|
+
current_sprint = $1.to_i
|
123
|
+
if current_sprint > sprint
|
124
|
+
self.sprint = current_sprint
|
125
|
+
end
|
126
|
+
end
|
127
|
+
burndown_data_path = File.join(burndown_dir, burndown_data_filename)
|
128
|
+
begin
|
129
|
+
read_data burndown_data_path
|
130
|
+
@burndown_data.board_id = board_id
|
131
|
+
@burndown_data.fetch
|
132
|
+
add_data(@burndown_data, Date.today)
|
133
|
+
write_data burndown_data_path
|
134
|
+
puts "Updated data for sprint #{self.sprint}"
|
135
|
+
rescue Errno::ENOENT
|
136
|
+
raise TrolloloError.new( "'#{burndown_data_path}' not found" )
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_next_sprint(burndown_dir)
|
141
|
+
Dir.glob("#{burndown_dir}/burndown-data-*.yaml").each do |file|
|
142
|
+
file =~ /burndown-data-(.*).yaml/
|
143
|
+
current_sprint = $1.to_i
|
144
|
+
if current_sprint > sprint
|
145
|
+
self.sprint = current_sprint
|
146
|
+
end
|
147
|
+
end
|
148
|
+
burndown_data_path = File.join(burndown_dir, burndown_data_filename)
|
149
|
+
begin
|
150
|
+
read_data burndown_data_path
|
151
|
+
self.sprint = self.sprint + 1
|
152
|
+
@data["days"] = []
|
153
|
+
write_data File.join(burndown_dir, burndown_data_filename)
|
154
|
+
rescue Errno::ENOENT
|
155
|
+
raise TrolloloError.new( "'#{burndown_data_path}' not found" )
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,172 @@
|
|
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
|
+
|
18
|
+
# This class represents the current state of burndown data on the Trello board.
|
19
|
+
# It encapsulates getting the data from Trello. It does not keep any history
|
20
|
+
# or interaction with the files used to generate burndown charts.
|
21
|
+
class BurndownData
|
22
|
+
|
23
|
+
attr_accessor :story_points, :tasks, :extra_story_points, :extra_tasks
|
24
|
+
attr_accessor :board_id
|
25
|
+
|
26
|
+
class Result
|
27
|
+
attr_accessor :open, :done
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@open = 0
|
31
|
+
@done = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def total
|
35
|
+
@open + @done
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize settings
|
40
|
+
@settings = settings
|
41
|
+
|
42
|
+
@story_points = Result.new
|
43
|
+
@tasks = Result.new
|
44
|
+
|
45
|
+
@extra_story_points = Result.new
|
46
|
+
@extra_tasks = Result.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def trello
|
50
|
+
Trello.new(board_id: @board_id, developer_public_key: @settings.developer_public_key, member_token: @settings.member_token)
|
51
|
+
end
|
52
|
+
|
53
|
+
def fetch_todo_list_id
|
54
|
+
lists = trello.lists
|
55
|
+
lists.each do |l|
|
56
|
+
if l["name"] =~ /^Sprint Backlog$/
|
57
|
+
return l["id"]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
raise "Unable to find sprint backlog column on sprint board"
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_doing_list_id
|
65
|
+
lists = trello.lists
|
66
|
+
lists.each do |l|
|
67
|
+
if l["name"] =~ /^Doing$/
|
68
|
+
return l["id"]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
raise "Unable to find doing column on sprint board"
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_done_list_id
|
76
|
+
lists = trello.lists
|
77
|
+
last_sprint = nil
|
78
|
+
lists.each do |l|
|
79
|
+
if l["name"] =~ /^Done Sprint (.*)$/
|
80
|
+
sprint = $1.to_i
|
81
|
+
if !last_sprint || sprint > last_sprint[:number]
|
82
|
+
last_sprint = { :number => sprint, :id => l["id"] }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
id = last_sprint[:id]
|
88
|
+
if !id
|
89
|
+
raise "Unable to find done column on sprint board"
|
90
|
+
end
|
91
|
+
id
|
92
|
+
end
|
93
|
+
|
94
|
+
def fetch
|
95
|
+
cards = trello.cards
|
96
|
+
|
97
|
+
todo_list_id = fetch_todo_list_id
|
98
|
+
doing_list_id = fetch_doing_list_id
|
99
|
+
done_list_id = fetch_done_list_id
|
100
|
+
|
101
|
+
if @settings.verbose
|
102
|
+
puts "Todo list: #{todo_list_id}"
|
103
|
+
puts "Doing list: #{doing_list_id}"
|
104
|
+
puts "Done list: #{done_list_id}"
|
105
|
+
end
|
106
|
+
|
107
|
+
sp_total = 0
|
108
|
+
sp_done = 0
|
109
|
+
|
110
|
+
extra_sp_total = 0
|
111
|
+
extra_sp_done = 0
|
112
|
+
|
113
|
+
tasks_total = 0
|
114
|
+
tasks_done = 0
|
115
|
+
|
116
|
+
extra_tasks_total = 0
|
117
|
+
extra_tasks_done = 0
|
118
|
+
|
119
|
+
cards.each do |c|
|
120
|
+
card = Card.parse c
|
121
|
+
|
122
|
+
list_id = c["idList"]
|
123
|
+
|
124
|
+
if list_id == todo_list_id || list_id == doing_list_id
|
125
|
+
if card.has_sp?
|
126
|
+
if card.extra?
|
127
|
+
extra_sp_total += card.sp
|
128
|
+
else
|
129
|
+
sp_total += card.sp
|
130
|
+
end
|
131
|
+
end
|
132
|
+
if card.extra?
|
133
|
+
extra_tasks_total += card.tasks
|
134
|
+
extra_tasks_done += card.tasks_done
|
135
|
+
else
|
136
|
+
tasks_total += card.tasks
|
137
|
+
tasks_done += card.tasks_done
|
138
|
+
end
|
139
|
+
elsif list_id == done_list_id
|
140
|
+
if card.has_sp?
|
141
|
+
if card.extra?
|
142
|
+
extra_sp_total += card.sp
|
143
|
+
extra_sp_done += card.sp
|
144
|
+
else
|
145
|
+
sp_total += card.sp
|
146
|
+
sp_done += card.sp
|
147
|
+
end
|
148
|
+
end
|
149
|
+
if card.extra?
|
150
|
+
extra_tasks_total += card.tasks
|
151
|
+
extra_tasks_done += card.tasks_done
|
152
|
+
else
|
153
|
+
tasks_total += card.tasks
|
154
|
+
tasks_done += card.tasks_done
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
@story_points.done = sp_done
|
160
|
+
@story_points.open = sp_total - sp_done
|
161
|
+
|
162
|
+
@tasks.done = tasks_done
|
163
|
+
@tasks.open = tasks_total - tasks_done
|
164
|
+
|
165
|
+
@extra_story_points.done = extra_sp_done
|
166
|
+
@extra_story_points.open = extra_sp_total - extra_sp_done
|
167
|
+
|
168
|
+
@extra_tasks.done = extra_tasks_done
|
169
|
+
@extra_tasks.open = extra_tasks_total - extra_tasks_done
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|