Merge lp:~bjornt/lp2kanban/create-bug-lane into lp:~launchpad/lp2kanban/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
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+157381@code.launchpad.net

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.
Revision history for this message
Brad Crittenden (bac) wrote :

This change looks good Bjorn. Thanks for the extra test coverage.

review: Approve (code)

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)

Subscribers

People subscribed via source and target branches