trollolo 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/.travis.yml +2 -2
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -1
- data/README.md +7 -3
- data/lib/card.rb +22 -0
- data/lib/cli.rb +47 -3
- data/lib/prioritizer.rb +34 -0
- data/lib/sprint_cleanup.rb +54 -0
- data/lib/trello_wrapper.rb +8 -0
- data/lib/trollolo.rb +2 -1
- data/lib/version.rb +1 -1
- data/scripts/burndowndata.py +7 -21
- data/spec/data/create_burndown_helper/burndown-01.png +0 -0
- data/spec/data/create_burndown_helper/burndown-23.png +0 -0
- data/spec/data/create_burndown_helper/burndown-31.png +0 -0
- data/spec/data/create_burndown_helper/burndown-42.png +0 -0
- data/spec/data/create_burndown_helper/burndown-56.png +0 -0
- data/spec/data/create_burndown_helper/burndown-data-01.yaml +23 -0
- data/spec/data/vcr/sprint_cleanup.yml +11021 -0
- data/spec/integration/create_burndown_spec.rb +4 -0
- data/spec/unit/card_spec.rb +40 -0
- data/spec/unit/prioritizer_spec.rb +47 -0
- data/spec/unit/sprint_cleanup_spec.rb +18 -0
- data/spec/unit/support/vcr.rb +52 -0
- data/spec/unit/trello_wrapper_spec.rb +1 -2
- metadata +12 -3
- data/lib/result.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1b0bc626175dfcdffe5dffa8942ec7469797ec4
|
4
|
+
data.tar.gz: a8feeb3163223a858044f9d2c9be8bb66e350699
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf9922a6a5d5ec348be19bde09f391115f6022e1fb0e2a2fc5b5f0c83cae7ee1b3de95037d3964b0fcbb54db1fb16c106a0f85df118f0928aed9f8f1424b7286
|
7
|
+
data.tar.gz: f5345fe994f6db7343aece171c5e57782264f493c454e742aba148f6e58c8d111eb714986ec3f2b2aa99d8bfd3c968b4644f2b16adb67a060b1ece658effa1f7
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Trollolo Changelog
|
2
2
|
|
3
|
+
## Version 0.0.9
|
4
|
+
|
5
|
+
* Add `sprint-cleanup` command to move cards back from the sprint board to the
|
6
|
+
planning board. It takes all cards from the "Sprint backlog" and "Doing"
|
7
|
+
columns on the sprint board, moves them to the "Ready" column on the planning
|
8
|
+
board and removes all members and the "under the waterline" label.
|
9
|
+
* Add `set-priorities` command to add priorities to the title of all cards of a
|
10
|
+
given column. The priorities are added as a prefix of the form "Pnn", where
|
11
|
+
"nn" is the number of the card in the column. This is useful, if you use the
|
12
|
+
order of cards as priorities and want to move them around to different columns
|
13
|
+
without losing this information.
|
14
|
+
* Consistently use hyphens in command names, get rid of underscores.
|
15
|
+
* Fix calculation of unplanned tasks on day one
|
16
|
+
* Fix scaling tasks
|
17
|
+
|
3
18
|
## Version 0.0.8
|
4
19
|
|
5
20
|
* Burndown chart reflects unplanned work
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -40,16 +40,20 @@ member_token: 87345897238957a29835789b2374580927f3589072398579820345
|
|
40
40
|
```
|
41
41
|
|
42
42
|
These values have to be set with the personal access data for the Trello API
|
43
|
-
and the
|
43
|
+
and the personal access token of the application, which has to be generated.
|
44
44
|
|
45
45
|
For creating a developer key go to the
|
46
46
|
[Developer API Keys](https://trello.com/1/appKey/generate) page on Trello. It's
|
47
47
|
the key in the first box.
|
48
48
|
|
49
|
-
For creating a member token use the following URL, replacing
|
49
|
+
For creating a member token use the following URL, replacing `...` by
|
50
50
|
the key you obtained in the first step.
|
51
51
|
|
52
|
-
https://trello.com/1/authorize?key
|
52
|
+
https://trello.com/1/authorize?key=...&name=trollolo&expiration=never&response_type=token
|
53
|
+
|
54
|
+
To create a member token for the `set-priority` command use this URL instead:
|
55
|
+
|
56
|
+
https://trello.com/1/connect?key=...&name=trollolo&response_type=token&scope=read,write
|
53
57
|
|
54
58
|
The board id is the cryptic string in the URL of your board.
|
55
59
|
|
data/lib/card.rb
CHANGED
@@ -18,6 +18,7 @@
|
|
18
18
|
class Card
|
19
19
|
# Assuming we have card titles as follows '(8) This is the card name'
|
20
20
|
ESTIMATED_REGEX = /\(([\d.]+)\)/
|
21
|
+
PRIORITY_REGEX = /^(?:\([\d.]+\) )?P(\d+): /
|
21
22
|
SPRINT_NUMBER_REGEX = /\ASprint (\d+)/
|
22
23
|
|
23
24
|
def initialize(board_data, card_id)
|
@@ -42,6 +43,19 @@ class Card
|
|
42
43
|
name.match(ESTIMATED_REGEX).captures.first.to_f
|
43
44
|
end
|
44
45
|
|
46
|
+
def priority
|
47
|
+
return unless m = name.match(PRIORITY_REGEX)
|
48
|
+
m.captures.first.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def priority=(n)
|
52
|
+
if priority
|
53
|
+
@card_data["name"].sub!(/P\d+: /, "P#{n}: ")
|
54
|
+
else
|
55
|
+
@card_data["name"] = "P#{n}: #{name}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
45
59
|
def done_tasks
|
46
60
|
count = 0
|
47
61
|
@card_data["checklists"].each do |checklist|
|
@@ -123,4 +137,12 @@ class Card
|
|
123
137
|
def name
|
124
138
|
@card_data["name"]
|
125
139
|
end
|
140
|
+
|
141
|
+
def name=(str)
|
142
|
+
@card_data["name"] = str
|
143
|
+
end
|
144
|
+
|
145
|
+
def id
|
146
|
+
@card_data["id"]
|
147
|
+
end
|
126
148
|
end
|
data/lib/cli.rb
CHANGED
@@ -206,7 +206,7 @@ EOT
|
|
206
206
|
b.backup(options["board-id"])
|
207
207
|
end
|
208
208
|
|
209
|
-
desc "
|
209
|
+
desc "list-backups", "List all backups"
|
210
210
|
def list_backups
|
211
211
|
b = Backup.new @@settings
|
212
212
|
b.list.each do |backup|
|
@@ -214,7 +214,7 @@ EOT
|
|
214
214
|
end
|
215
215
|
end
|
216
216
|
|
217
|
-
desc "
|
217
|
+
desc "show-backup", "Show backup of board"
|
218
218
|
option "board-id", :desc => "Id of Trello board", :required => true
|
219
219
|
option "show-descriptions", :desc => "Show descriptions of cards", :required => false, :type => :boolean
|
220
220
|
def show_backup
|
@@ -257,7 +257,7 @@ EOT
|
|
257
257
|
trello.set_description(options["card-id"], STDIN.read)
|
258
258
|
end
|
259
259
|
|
260
|
-
desc "
|
260
|
+
desc "organization-members", "Show organization members"
|
261
261
|
option "org-name", :desc => "Name of organization", :required => true
|
262
262
|
def organization_members
|
263
263
|
process_global_options options
|
@@ -293,6 +293,50 @@ EOT
|
|
293
293
|
trello.make_cover(options["card-id"], filename)
|
294
294
|
end
|
295
295
|
|
296
|
+
desc "set-priorities", "Set priority text into card titles"
|
297
|
+
long_desc <<EOT
|
298
|
+
Add 'P<n>: ' to the beginning of every cards title, replace where
|
299
|
+
already present. n is the current position of the list on the card.
|
300
|
+
EOT
|
301
|
+
option "board-id", :desc => "Id of the board", :required => true
|
302
|
+
option "list-name", :desc => "Name of the list", :required => true
|
303
|
+
def set_priorities
|
304
|
+
process_global_options options
|
305
|
+
require_trello_credentials
|
306
|
+
|
307
|
+
p = Prioritizer.new(@@settings)
|
308
|
+
p.prioritize(options["board-id"], options["list-name"])
|
309
|
+
end
|
310
|
+
|
311
|
+
desc "list-member-boards", "List name and id of all boards"
|
312
|
+
option "member-id", :desc => "Id of the member", :required => true
|
313
|
+
def list_member_boards
|
314
|
+
process_global_options options
|
315
|
+
require_trello_credentials
|
316
|
+
|
317
|
+
trello = TrelloWrapper.new(@@settings)
|
318
|
+
trello.get_member_boards(options["member-id"]).sort_by { |board|
|
319
|
+
board["name"]
|
320
|
+
}.each { |board|
|
321
|
+
puts "#{board["name"]} - #{board["id"]}"
|
322
|
+
}
|
323
|
+
end
|
324
|
+
|
325
|
+
desc "sprint-cleanup", "Move remaining cards to backlog"
|
326
|
+
long_desc <<EOT
|
327
|
+
After the sprint, move remaining cards from 'Sprint Backlog' and 'Doing'
|
328
|
+
back to the planning board into the 'Ready' list.
|
329
|
+
EOT
|
330
|
+
option "board-id", :desc => "Id of the board", :required => true
|
331
|
+
option "target-board-id", :desc => "Id of the target board", :required => true
|
332
|
+
def sprint_cleanup
|
333
|
+
process_global_options options
|
334
|
+
require_trello_credentials
|
335
|
+
|
336
|
+
s = SprintCleanup.new(@@settings)
|
337
|
+
s.cleanup(options["board-id"], options["target-board-id"])
|
338
|
+
end
|
339
|
+
|
296
340
|
private
|
297
341
|
|
298
342
|
def process_global_options options
|
data/lib/prioritizer.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
class Prioritizer
|
2
|
+
|
3
|
+
def initialize(settings)
|
4
|
+
@settings = settings
|
5
|
+
end
|
6
|
+
|
7
|
+
def prioritize(board_id, list_name)
|
8
|
+
list = find_list(board_id, list_name)
|
9
|
+
fail "list not found on board" unless list
|
10
|
+
update_priorities(list)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def trello
|
16
|
+
@trello ||= TrelloWrapper.new(@settings)
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_priorities(list)
|
20
|
+
n = 1
|
21
|
+
list.cards.each do |card|
|
22
|
+
next if card.name =~ /waterline/i
|
23
|
+
card.priority = n
|
24
|
+
trello.set_name(card.id, card.name)
|
25
|
+
puts %(set priority to #{n} for "#{card.name}")
|
26
|
+
n += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_list(board_id, list_name)
|
31
|
+
board = trello.board(board_id)
|
32
|
+
board.columns.find { |list| list.name == list_name }
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class SprintCleanup
|
2
|
+
SOURCE_LISTS = ["Sprint Backlog", "Doing"]
|
3
|
+
TARGET_LIST = "Ready"
|
4
|
+
|
5
|
+
def initialize(settings)
|
6
|
+
@settings = settings
|
7
|
+
init_trello
|
8
|
+
end
|
9
|
+
|
10
|
+
def cleanup(board_id, target_board_id)
|
11
|
+
@board = Trello::Board.find(board_id)
|
12
|
+
@target_board = Trello::Board.find(target_board_id)
|
13
|
+
|
14
|
+
SOURCE_LISTS.each do |list_name|
|
15
|
+
move_cards(@board.lists.find { |l| l.name == list_name })
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def init_trello
|
22
|
+
Trello.configure do |config|
|
23
|
+
config.developer_public_key = @settings.developer_public_key
|
24
|
+
config.member_token = @settings.member_token
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def target_list
|
29
|
+
@target_list ||= @target_board.lists.find { |l| l.name == TARGET_LIST }
|
30
|
+
end
|
31
|
+
|
32
|
+
def sticky?(card)
|
33
|
+
card.labels.any? { |l| l.name == "Sticky" }
|
34
|
+
end
|
35
|
+
|
36
|
+
def waterline_label(card)
|
37
|
+
card.labels.find { |label| label.name =~ /waterline/i }
|
38
|
+
end
|
39
|
+
|
40
|
+
def remove_waterline_label(card)
|
41
|
+
label = waterline_label(card)
|
42
|
+
card.remove_label(label) if label
|
43
|
+
end
|
44
|
+
|
45
|
+
def move_cards(source_list)
|
46
|
+
source_list.cards.each do |card|
|
47
|
+
next if sticky?(card)
|
48
|
+
puts %(moving card "#{card.name}" to list "#{target_list.name}")
|
49
|
+
card.members.each { |member| card.remove_member(member) }
|
50
|
+
remove_waterline_label(card)
|
51
|
+
card.move_to_board(@target_board, target_list)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/trello_wrapper.rb
CHANGED
@@ -76,10 +76,18 @@ class TrelloWrapper
|
|
76
76
|
card.desc
|
77
77
|
end
|
78
78
|
|
79
|
+
def get_member_boards(member_id)
|
80
|
+
JSON.parse(client.get("/members/#{member_id}/boards"))
|
81
|
+
end
|
82
|
+
|
79
83
|
def set_description(card_id, description)
|
80
84
|
client.put("/cards/#{card_id}/desc?value=#{description}")
|
81
85
|
end
|
82
86
|
|
87
|
+
def set_name(card_id, name)
|
88
|
+
client.put("/cards/#{card_id}/name?value=#{name}")
|
89
|
+
end
|
90
|
+
|
83
91
|
private
|
84
92
|
|
85
93
|
def init_trello
|
data/lib/trollolo.rb
CHANGED
@@ -27,12 +27,13 @@ require_relative 'settings'
|
|
27
27
|
require_relative 'column'
|
28
28
|
require_relative 'card'
|
29
29
|
require_relative 'scrum_board'
|
30
|
-
require_relative 'result'
|
31
30
|
require_relative 'trello_wrapper'
|
32
31
|
require_relative 'burndown_chart'
|
33
32
|
require_relative 'burndown_data'
|
34
33
|
require_relative 'backup'
|
35
34
|
require_relative 'checklist'
|
35
|
+
require_relative 'prioritizer'
|
36
|
+
require_relative 'sprint_cleanup'
|
36
37
|
|
37
38
|
class TrolloloError < StandardError
|
38
39
|
end
|
data/lib/version.rb
CHANGED
data/scripts/burndowndata.py
CHANGED
@@ -9,14 +9,12 @@ class BurndownData:
|
|
9
9
|
self.args = args
|
10
10
|
burndown = self.readYAML(self.args.sprint)
|
11
11
|
self.getSprintData(burndown)
|
12
|
-
self.calculateMaxStoryPoints()
|
13
|
-
self.calculateMaxTasks()
|
14
12
|
self.setBonusTasksDayOne(burndown)
|
15
13
|
self.setUnplannedTasksDayOne(burndown)
|
16
14
|
self.setExtraDays()
|
17
15
|
self.setUnplannedDays()
|
18
|
-
self.calculateYRange(self.
|
19
|
-
self.setScaleFactor(self.
|
16
|
+
self.calculateYRange(self.total_story_points[0], self.bonus_tasks_done, self.bonus_story_points_done, self.unplanned_tasks_done, self.unplanned_story_points_done)
|
17
|
+
self.setScaleFactor(self.total_tasks[0], self.total_story_points[0])
|
20
18
|
|
21
19
|
def readYAML(self, sprint_number):
|
22
20
|
with open('burndown-data-' + sprint_number + '.yaml', 'r') as f:
|
@@ -49,8 +47,6 @@ class BurndownData:
|
|
49
47
|
self.y_fast_lane = []
|
50
48
|
self.total_fast_lane = []
|
51
49
|
self.total_unplanned_fast_lane = []
|
52
|
-
self.max_story_points = 0
|
53
|
-
self.max_tasks = 0
|
54
50
|
|
55
51
|
for day in burndown["days"]:
|
56
52
|
self.days.append(self.current_day)
|
@@ -94,16 +90,6 @@ class BurndownData:
|
|
94
90
|
self.current_day += 1
|
95
91
|
return
|
96
92
|
|
97
|
-
def calculateMaxStoryPoints(self):
|
98
|
-
for sp in self.total_story_points:
|
99
|
-
self.max_story_points = max(self.max_story_points, sp)
|
100
|
-
return
|
101
|
-
|
102
|
-
def calculateMaxTasks(self):
|
103
|
-
for t in self.total_tasks:
|
104
|
-
self.max_tasks = max(self.max_tasks, t)
|
105
|
-
return
|
106
|
-
|
107
93
|
def setBonusTasksDayOne(self, burndown):
|
108
94
|
if burndown["days"][0].has_key("tasks_extra"):
|
109
95
|
self.bonus_tasks_day_one = burndown["days"][0]["tasks_extra"]["done"]
|
@@ -113,7 +99,7 @@ class BurndownData:
|
|
113
99
|
|
114
100
|
def setUnplannedTasksDayOne(self, burndown):
|
115
101
|
if burndown["days"][0].has_key("unplanned_tasks"):
|
116
|
-
self.unplanned_tasks_day_one = burndown["days"][0]["unplanned_tasks"]["
|
102
|
+
self.unplanned_tasks_day_one = burndown["days"][0]["unplanned_tasks"]["total"] - burndown["days"][0]["unplanned_tasks"]["open"]
|
117
103
|
else:
|
118
104
|
self.unplanned_tasks_day_one = 0
|
119
105
|
return
|
@@ -140,8 +126,8 @@ class BurndownData:
|
|
140
126
|
self.unplanned_day = 1
|
141
127
|
return
|
142
128
|
|
143
|
-
def calculateYRange(self,
|
144
|
-
self.ymax =
|
129
|
+
def calculateYRange(self, total_story_points, bonus_tasks_done, bonus_story_points_done, unplanned_tasks_done, unplanned_story_points_done):
|
130
|
+
self.ymax = total_story_points + 3
|
145
131
|
|
146
132
|
if len(bonus_tasks_done) > 0:
|
147
133
|
ymin_bonus_tasks = min(bonus_tasks_done) -3
|
@@ -168,7 +154,7 @@ class BurndownData:
|
|
168
154
|
self.ymin = -3
|
169
155
|
return
|
170
156
|
|
171
|
-
def setScaleFactor(self,
|
172
|
-
self.scalefactor = float(
|
157
|
+
def setScaleFactor(self, total_tasks, total_story_points):
|
158
|
+
self.scalefactor = float(total_tasks) / float(total_story_points)
|
173
159
|
return
|
174
160
|
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,23 @@
|
|
1
|
+
---
|
2
|
+
meta:
|
3
|
+
board_id: CRdddpdy
|
4
|
+
sprint: 1
|
5
|
+
total_days: 10
|
6
|
+
weekend_lines:
|
7
|
+
- 3.5
|
8
|
+
- 8.5
|
9
|
+
days:
|
10
|
+
- date: '2016-03-17'
|
11
|
+
updated_at: '2016-03-17T18:31:46+01:00'
|
12
|
+
story_points:
|
13
|
+
total: 16.0
|
14
|
+
open: 13.0
|
15
|
+
tasks:
|
16
|
+
total: 13
|
17
|
+
open: 9
|
18
|
+
unplanned_story_points:
|
19
|
+
total: 3.0
|
20
|
+
open: 1.0
|
21
|
+
unplanned_tasks:
|
22
|
+
total: 2
|
23
|
+
open: 1
|