trollolo 0.0.3
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.
- 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
|
+
[](https://travis-ci.org/openSUSE/trollolo)
|
4
|
+
[](https://codeclimate.com/github/openSUSE/trollolo)
|
5
|
+
[](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
|