Merge lp:~bjornt/lp2kanban/create-bug-lane into lp:~launchpad/lp2kanban/trunk
- create-bug-lane
- Merge into trunk
Proposed by
Björn Tillenius
Status: | Merged |
---|---|
Approved by: | Björn Tillenius |
Approved revision: | 82 |
Merged at revision: | 82 |
Proposed branch: | lp:~bjornt/lp2kanban/create-bug-lane |
Merge into: | lp:~launchpad/lp2kanban/trunk |
Diff against target: |
477 lines (+203/-39) 4 files modified
examples/config.ini (+4/-0) src/lp2kanban/bugs2cards.py (+10/-5) src/lp2kanban/tests/common.py (+31/-11) src/lp2kanban/tests/test_bugs2cards.py (+158/-23) |
To merge this branch: | bzr merge lp:~bjornt/lp2kanban/create-bug-lane |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email: mp+157381@code.launchpad.net |
Commit message
Description of the change
Add a config option for specifying where new cards should be added.
When a card is added from a bug, the first lane was chosen. This
isn't always what you want, so with this change it's possible to
choose a specific lane.
I also took the opportunity to add tests for create_new_cards.
That's why the diff is quite large.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'examples/config.ini' |
2 | --- examples/config.ini 2013-04-04 09:20:18 +0000 |
3 | +++ examples/config.ini 2013-04-05 14:28:23 +0000 |
4 | @@ -27,6 +27,10 @@ |
5 | # card type is used. |
6 | #bug_to_card_type = Defect |
7 | |
8 | +# Which lane cards created from bugs should be added to. If not set, the |
9 | +# first lane is used |
10 | +#bug_to_card_lane = Incoming |
11 | + |
12 | # Lanes are assigned a "role" by looking for sublanes matching |
13 | # values given here. "Coding" will match all lanes which have |
14 | # a title of "Coding", and "QA::Ready" will match all lanes |
15 | |
16 | === modified file 'src/lp2kanban/bugs2cards.py' |
17 | --- src/lp2kanban/bugs2cards.py 2013-04-04 09:20:18 +0000 |
18 | +++ src/lp2kanban/bugs2cards.py 2013-04-05 14:28:23 +0000 |
19 | @@ -363,7 +363,8 @@ |
20 | return target_lane |
21 | return None |
22 | |
23 | -def create_new_cards(board, launchpad_project, bug_tag, cardtype_name=None): |
24 | +def create_new_cards(board, launchpad_project, bug_tag, cardtype_name=None, |
25 | + cardlane_path=None): |
26 | """Create cards for bugs tagged in Launchpad. |
27 | |
28 | If a bug is tagged with the given tag, a new card will be created in |
29 | @@ -377,9 +378,12 @@ |
30 | kanban_bugtasks = launchpad_project.searchTasks(tags=[bug_tag]) |
31 | kanban_bugs = get_new_bugs( |
32 | board, [bugtask.bug for bugtask in kanban_bugtasks]) |
33 | - next_lane = board.root_lane |
34 | - while next_lane.child_lanes: |
35 | - next_lane = next_lane.child_lanes[0] |
36 | + if cardlane_path is not None: |
37 | + next_lane = board.getLaneByPath(cardlane_path) |
38 | + else: |
39 | + next_lane = board.root_lane |
40 | + while next_lane.child_lanes: |
41 | + next_lane = next_lane.child_lanes[0] |
42 | for bug in kanban_bugs: |
43 | print " Creating card for bug %s in %s." % (bug.id, next_lane.title) |
44 | card = next_lane.addCard() |
45 | @@ -436,7 +440,8 @@ |
46 | for project in all_projects: |
47 | print " Creating new cards for %s:" % project.name |
48 | create_new_cards( |
49 | - board, project, new_bug_tag, bconf.get("bug_to_card_type")) |
50 | + board, project, new_bug_tag, bconf.get("bug_to_card_type"), |
51 | + bconf.get("bug_to_card_lane")) |
52 | print " Syncing cards:" |
53 | should_move_cards = bconf.get('move_cards') == 'on' |
54 | for card in board.getCardsWithExternalIds(): |
55 | |
56 | === modified file 'src/lp2kanban/tests/common.py' |
57 | --- src/lp2kanban/tests/common.py 2013-04-04 11:51:48 +0000 |
58 | +++ src/lp2kanban/tests/common.py 2013-04-05 14:28:23 +0000 |
59 | @@ -8,16 +8,19 @@ |
60 | |
61 | class FauxCardType: |
62 | |
63 | - def __init__(self): |
64 | - self.id = 1 |
65 | - self.name = "Test" |
66 | - self.is_default = True |
67 | + def __init__(self, id=1, name='Test', is_default=True): |
68 | + self.id = id |
69 | + self.name = name |
70 | + self.is_default = is_default |
71 | self.ColourHex = "#000" |
72 | self.IconPath = None |
73 | |
74 | |
75 | class FauxBoard: |
76 | |
77 | + root_lane = None |
78 | + _next_lane_id = 0 |
79 | + |
80 | def __init__(self, cards=None, users=None, users_by_id=None, |
81 | default_cardtype=None, is_archived=False): |
82 | if cards is None: |
83 | @@ -25,12 +28,13 @@ |
84 | self.cards = cards |
85 | self.users = users |
86 | self.users_by_id = users_by_id |
87 | - self.default_cardtype = default_cardtype or FauxCardType() |
88 | - self.cardtypes = {1: FauxCardType()} |
89 | + self.default_cardtype = default_cardtype or FauxCardType(id=1) |
90 | + self.cardtypes = {self.default_cardtype.id: self.default_cardtype} |
91 | self.is_archived = is_archived |
92 | self._cards_with_description_annotations = set() |
93 | self._cards_with_external_links = set() |
94 | self.lanes = {} |
95 | + self.root_lane = self.addLane('ROOT LANE') |
96 | |
97 | def getCardsWithDescriptionAnnotations(self): |
98 | return self._cards_with_description_annotations |
99 | @@ -38,20 +42,33 @@ |
100 | def getCardsWithExternalLinks(self): |
101 | return self._cards_with_external_links |
102 | |
103 | + def getLaneByPath(self, path): |
104 | + for lane in self.lanes.values(): |
105 | + if lane.path == path: |
106 | + return lane |
107 | + return None |
108 | + |
109 | def addLane(self, path): |
110 | - lane = FauxLane(path, board=self) |
111 | - self.lanes[path] = lane |
112 | + lane = FauxLane(path, board=self, id=self._next_lane_id) |
113 | + self._next_lane_id += 1 |
114 | + if self.root_lane is not None: |
115 | + self.root_lane.child_lanes.append(lane) |
116 | + self.lanes[lane.id] = lane |
117 | return lane |
118 | |
119 | |
120 | class FauxLane: |
121 | |
122 | - def __init__(self, path=None, id=None, type=None, board=None): |
123 | + def __init__(self, path=None, title=None, id=None, type=None, board=None): |
124 | self.id = id or 1 |
125 | self.type = type |
126 | self.board = board or FauxBoard("Test") |
127 | self.path = path |
128 | self.next_lanes = None |
129 | + self.child_lanes = [] |
130 | + if title is None: |
131 | + title = path |
132 | + self.title = title |
133 | |
134 | def getNextLanes(self): |
135 | return self.next_lanes |
136 | @@ -62,7 +79,9 @@ |
137 | else: |
138 | self.next_lanes.append(lane) |
139 | |
140 | - def addCard(self, card): |
141 | + def addCard(self, card=None): |
142 | + if card is None: |
143 | + card = FauxCard() |
144 | card.lane = self |
145 | if self.board is not None: |
146 | self.board.cards.append(card) |
147 | @@ -83,10 +102,11 @@ |
148 | self.external_system_name = external_system_name |
149 | self.external_system_url = external_system_url |
150 | self.moved_to = None |
151 | + self.saved = False |
152 | |
153 | def save(self): |
154 | """A no-op for compatibility.""" |
155 | - return |
156 | + self.saved = True |
157 | |
158 | def move(self, target_lane): |
159 | self.moved_to = target_lane |
160 | |
161 | === modified file 'src/lp2kanban/tests/test_bugs2cards.py' |
162 | --- src/lp2kanban/tests/test_bugs2cards.py 2013-04-04 11:51:48 +0000 |
163 | +++ src/lp2kanban/tests/test_bugs2cards.py 2013-04-05 14:28:23 +0000 |
164 | @@ -15,6 +15,7 @@ |
165 | |
166 | from lp2kanban.bugs2cards import ( |
167 | CardStatus, |
168 | + create_new_cards, |
169 | find_card_target_lane_next, |
170 | get_branch_info, |
171 | get_bug_status, |
172 | @@ -34,6 +35,7 @@ |
173 | from lp2kanban.tests.common import ( |
174 | FauxBoard, |
175 | FauxCard, |
176 | + FauxCardType, |
177 | FauxLane, |
178 | ) |
179 | |
180 | @@ -254,10 +256,26 @@ |
181 | self.title = title |
182 | self.description = description |
183 | self.id = bug_id |
184 | + self.saved = False |
185 | |
186 | def addTask(self, target, status='New', assignee=None): |
187 | - self.bug_tasks_collection.append( |
188 | - Record(target=target, status=status, assignee=assignee)) |
189 | + record = Record(target=target, status=status, assignee=assignee) |
190 | + self.bug_tasks_collection.append(record) |
191 | + return record |
192 | + |
193 | + def lp_save(self): |
194 | + self.saved = True |
195 | + |
196 | + |
197 | +class FauxBugTask: |
198 | + |
199 | + def __init__(self, bug_id, target='faux-project', tags=None, |
200 | + title="Faux Bug", description="This is a faux bug."): |
201 | + if tags is None: |
202 | + tags = [] |
203 | + self.bug = FauxBug( |
204 | + bug_id=bug_id, title=title, description=description, tags=tags) |
205 | + self.record = self.bug.addTask(target) |
206 | |
207 | |
208 | class BugStatusTest(unittest.TestCase): |
209 | @@ -557,6 +575,123 @@ |
210 | self.assertEqual([bugs[1]], list(get_new_bugs(board, bugs))) |
211 | |
212 | |
213 | +class FauxLaunchpadProject: |
214 | + |
215 | + def __init__(self, bug_tasks=None): |
216 | + if bug_tasks is None: |
217 | + bug_tasks = [] |
218 | + self.bug_tasks = bug_tasks |
219 | + |
220 | + def searchTasks(self, tags=None): |
221 | + bug_tasks = self.bug_tasks |
222 | + if tags is not None: |
223 | + tags = set(tags) |
224 | + bug_tasks = [ |
225 | + bug_task for bug_task in bug_tasks |
226 | + if tags.intersection(bug_task.bug.tags) == tags] |
227 | + return bug_tasks |
228 | + |
229 | + |
230 | +class CreateNewCardsTest(unittest.TestCase): |
231 | + |
232 | + def test_no_bugs(self): |
233 | + # If no bugs are found, no cards are created. |
234 | + board = FauxBoard() |
235 | + bug_lane = board.addLane('bugs') |
236 | + project = FauxLaunchpadProject(bug_tasks=[]) |
237 | + create_new_cards(board, project, 'kanban') |
238 | + self.assertEqual([], board.cards) |
239 | + |
240 | + def test_new_bugs_card_added(self): |
241 | + # If a new bug is found, a card is created for it, having the |
242 | + # bug id, title and description saved. |
243 | + board = FauxBoard() |
244 | + bug_lane = board.addLane('bugs') |
245 | + project = FauxLaunchpadProject( |
246 | + bug_tasks=[FauxBugTask( |
247 | + bug_id=42, title='Test bug', |
248 | + description='Test bug description.', |
249 | + tags=['kanban'])]) |
250 | + create_new_cards(board, project, 'kanban') |
251 | + [card] = board.cards |
252 | + self.assertEqual('Test bug', card.title) |
253 | + self.assertEqual('Test bug description.', card.description) |
254 | + self.assertEqual('42', card.external_card_id) |
255 | + self.assertEqual(board.default_cardtype.id, card.type_id) |
256 | + self.assertTrue(card.saved) |
257 | + |
258 | + def test_new_bugs_tag_removed(self): |
259 | + # After a card is created from a bug, the bug tag is removed, so |
260 | + # that having a lot of open bugs won't slow down the process. |
261 | + board = FauxBoard() |
262 | + bug_lane = board.addLane('bugs') |
263 | + project = FauxLaunchpadProject( |
264 | + bug_tasks=[FauxBugTask( |
265 | + bug_id=42, title='Test bug', |
266 | + description='Test bug description.', |
267 | + tags=['kanban'])]) |
268 | + create_new_cards(board, project, 'kanban') |
269 | + [bug_task] = project.bug_tasks |
270 | + self.assertEqual([], bug_task.bug.tags) |
271 | + self.assertTrue(bug_task.bug.saved) |
272 | + |
273 | + def test_new_bugs_tagged_only(self): |
274 | + # Cards are created only for bugs having the bug tag that is |
275 | + # passed in to create_new_cards(). |
276 | + board = FauxBoard() |
277 | + bug_lane = board.addLane('bugs') |
278 | + project = FauxLaunchpadProject( |
279 | + bug_tasks=[FauxBugTask( |
280 | + bug_id=21, title='Tagged with other tag', |
281 | + tags=['sometag']), |
282 | + FauxBugTask( |
283 | + bug_id=42, title='Tagged with right tag', |
284 | + tags=['kanban']), |
285 | + FauxBugTask( |
286 | + bug_id=42, title='Tagged with no tags', |
287 | + tags=[])]) |
288 | + create_new_cards(board, project, 'kanban') |
289 | + [card] = board.cards |
290 | + self.assertEqual('Tagged with right tag', card.title) |
291 | + self.assertEqual('42', card.external_card_id) |
292 | + |
293 | + def test_new_bugs_custom_card_type(self): |
294 | + # It's possible to specify which card type that the created card |
295 | + # should have, instead of the default one. |
296 | + board = FauxBoard() |
297 | + bug_lane = board.addLane('bugs') |
298 | + project = FauxLaunchpadProject( |
299 | + bug_tasks=[FauxBugTask( |
300 | + bug_id=42, title='Test bug', |
301 | + description='Test bug description.', |
302 | + tags=['kanban'])]) |
303 | + feature_type = FauxCardType(id=2, name='Feature', is_default=False) |
304 | + board.cardtypes[feature_type.id] = feature_type |
305 | + create_new_cards(board, project, 'kanban', cardtype_name='Feature') |
306 | + [card] = board.cards |
307 | + self.assertEqual(feature_type.id, card.type_id) |
308 | + self.assertTrue(card.saved) |
309 | + |
310 | + def test_new_bugs_custom_card_lane(self): |
311 | + # It's possible to specify to which lane the card should be |
312 | + # added. |
313 | + board = FauxBoard() |
314 | + bug_lane = board.addLane('bugs') |
315 | + bug_lane = board.addLane('incoming') |
316 | + project = FauxLaunchpadProject( |
317 | + bug_tasks=[FauxBugTask( |
318 | + bug_id=42, title='Test bug', |
319 | + description='Test bug description.', |
320 | + tags=['kanban'])]) |
321 | + feature_type = FauxCardType(id=2, name='Feature', is_default=False) |
322 | + board.cardtypes[feature_type.id] = feature_type |
323 | + create_new_cards( |
324 | + board, project, 'kanban', cardtype_name='Feature', |
325 | + cardlane_path='incoming') |
326 | + [card] = board.cards |
327 | + self.assertEqual('incoming', card.lane.path) |
328 | + |
329 | + |
330 | class GroupsConfigTest(unittest.TestCase): |
331 | |
332 | def test_parse_group_conf_empty_file(self): |
333 | @@ -693,11 +828,11 @@ |
334 | "done_lanes": "done", |
335 | } |
336 | self.board = FauxBoard() |
337 | - self.board.addLane("backlog") |
338 | + self.backlog = self.board.addLane("backlog") |
339 | self.board.addLane("coding") |
340 | - self.board.addLane("review") |
341 | + self.review = self.board.addLane("review") |
342 | self.board.addLane("landing") |
343 | - self.board.addLane("qa") |
344 | + self.qa = self.board.addLane("qa") |
345 | self.board.addLane("deploy") |
346 | self.board.addLane("downtime") |
347 | self.board.addLane("done") |
348 | @@ -705,51 +840,51 @@ |
349 | |
350 | def test_coding(self): |
351 | # CODING cards are moved to the coding lane. |
352 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
353 | + card = self.backlog.addCard(FauxCard()) |
354 | move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users) |
355 | self.assertEqual("coding", card.moved_to.path) |
356 | |
357 | def test_review(self): |
358 | # REVIEW cards are moved to the review lane. |
359 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
360 | + card = self.backlog.addCard(FauxCard()) |
361 | move_card(card, CardStatus.REVIEW, self.bconf, [], self.lp_users) |
362 | self.assertEqual("review", card.moved_to.path) |
363 | |
364 | def test_landing(self): |
365 | # LANDING cards are moved to the landing lane. |
366 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
367 | + card = self.backlog.addCard(FauxCard()) |
368 | move_card(card, CardStatus.LANDING, self.bconf, [], self.lp_users) |
369 | self.assertEqual("landing", card.moved_to.path) |
370 | |
371 | def test_qa(self): |
372 | # QA cards are moved to the qa lane. |
373 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
374 | + card = self.backlog.addCard(FauxCard()) |
375 | move_card(card, CardStatus.QA, self.bconf, [], self.lp_users) |
376 | self.assertEqual("qa", card.moved_to.path) |
377 | |
378 | def test_deploy(self): |
379 | # DEPLOY cards are moved to the deploy lane. |
380 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
381 | + card = self.backlog.addCard(FauxCard()) |
382 | move_card(card, CardStatus.DEPLOY, self.bconf, [], self.lp_users) |
383 | self.assertEqual("deploy", card.moved_to.path) |
384 | |
385 | def test_downtime(self): |
386 | # DOWNTIME_DEPLOY cards are moved to the downtime lane. |
387 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
388 | + card = self.backlog.addCard(FauxCard()) |
389 | move_card( |
390 | card, CardStatus.DOWNTIME_DEPLOY, self.bconf, [], self.lp_users) |
391 | self.assertEqual("downtime", card.moved_to.path) |
392 | |
393 | def test_done(self): |
394 | # DONE cards are moved to the done lane. |
395 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
396 | + card = self.backlog.addCard(FauxCard()) |
397 | move_card(card, CardStatus.DONE, self.bconf, [], self.lp_users) |
398 | self.assertEqual("done", card.moved_to.path) |
399 | |
400 | def test_backwards(self): |
401 | # Cards can be moved backwards as well, e.g. go from QA to |
402 | # CODING, if an issue is found. |
403 | - card = self.board.lanes["qa"].addCard(FauxCard()) |
404 | + card = self.qa.addCard(FauxCard()) |
405 | move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users) |
406 | self.assertEqual("coding", card.moved_to.path) |
407 | |
408 | @@ -758,7 +893,7 @@ |
409 | # an assignee, the card isn't moved. |
410 | self.bconf["coding_lanes"] = "${group}::coding" |
411 | self.board.addLane("test group::coding") |
412 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
413 | + card = self.backlog.addCard(FauxCard()) |
414 | move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users) |
415 | self.assertEqual(None, card.moved_to) |
416 | |
417 | @@ -768,7 +903,7 @@ |
418 | # group's lane. |
419 | self.bconf["coding_lanes"] = "${group}::coding" |
420 | self.board.addLane("test group::coding") |
421 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
422 | + card = self.backlog.addCard(FauxCard()) |
423 | self.lp_users.lp_to_group["foo"] = "test group" |
424 | move_card( |
425 | card, CardStatus.CODING, self.bconf, [FauxPerson("foo")], |
426 | @@ -780,7 +915,7 @@ |
427 | # assignee that is not part of a group, the card isn't moved. |
428 | self.bconf["coding_lanes"] = "${group}::coding" |
429 | self.board.addLane("test group::coding") |
430 | - card = self.board.lanes["backlog"].addCard(FauxCard()) |
431 | + card = self.backlog.addCard(FauxCard()) |
432 | self.lp_users.lp_to_group["foo"] = "test group" |
433 | move_card( |
434 | card, CardStatus.CODING, self.bconf, [FauxPerson("bar")], |
435 | @@ -790,7 +925,7 @@ |
436 | def test_manual_lanes(self): |
437 | # Cards in a "manual" lane doesn't get moved by move_card(). |
438 | self.bconf["manual_lanes"] = "qa" |
439 | - card = self.board.lanes["qa"].addCard(FauxCard()) |
440 | + card = self.qa.addCard(FauxCard()) |
441 | move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users) |
442 | self.assertEqual(None, card.moved_to) |
443 | |
444 | @@ -798,9 +933,9 @@ |
445 | # Partial lane names can be specified in the "manual_lanes" |
446 | # config, so that every group's QA lane can be set to manual, |
447 | # for example. |
448 | - self.board.addLane("dev::qa") |
449 | + dev_qa = self.board.addLane("dev::qa") |
450 | self.bconf["manual_lanes"] = "qa" |
451 | - card = self.board.lanes["dev::qa"].addCard(FauxCard()) |
452 | + card = dev_qa.addCard(FauxCard()) |
453 | move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users) |
454 | self.assertEqual(None, card.moved_to) |
455 | |
456 | @@ -808,8 +943,8 @@ |
457 | # Multiple "manual" lanes can be separated by comma in the |
458 | # config. |
459 | self.bconf["manual_lanes"] = "qa,review" |
460 | - card1 = self.board.lanes["qa"].addCard(FauxCard()) |
461 | - card2 = self.board.lanes["review"].addCard(FauxCard()) |
462 | + card1 = self.qa.addCard(FauxCard()) |
463 | + card2 = self.review.addCard(FauxCard()) |
464 | move_card(card1, CardStatus.CODING, self.bconf, [], self.lp_users) |
465 | move_card(card2, CardStatus.CODING, self.bconf, [], self.lp_users) |
466 | self.assertEqual(None, card1.moved_to) |
467 | @@ -819,8 +954,8 @@ |
468 | # Whitespace is ignored when specifying multiple lanes in the |
469 | # "manual_lanes" config. |
470 | self.bconf["manual_lanes"] = " qa , review " |
471 | - card1 = self.board.lanes["qa"].addCard(FauxCard()) |
472 | - card2 = self.board.lanes["review"].addCard(FauxCard()) |
473 | + card1 = self.qa.addCard(FauxCard()) |
474 | + card2 = self.review.addCard(FauxCard()) |
475 | move_card(card1, CardStatus.CODING, self.bconf, [], self.lp_users) |
476 | move_card(card2, CardStatus.CODING, self.bconf, [], self.lp_users) |
477 | self.assertEqual(None, card1.moved_to) |
This change looks good Bjorn. Thanks for the extra test coverage.