Merge lp:~chad.smith/lp2kanban/kanban-cards-in-feature-owners-next into lp:lp2kanban

Proposed by Chad Smith
Status: Merged
Approved by: Chad Smith
Approved revision: 129
Merged at revision: 125
Proposed branch: lp:~chad.smith/lp2kanban/kanban-cards-in-feature-owners-next
Merge into: lp:lp2kanban
Diff against target: 341 lines (+164/-32)
3 files modified
src/lp2kanban/bugs2cards.py (+46/-12)
src/lp2kanban/tests/common.py (+4/-1)
src/lp2kanban/tests/test_bugs2cards.py (+114/-19)
To merge this branch: bzr merge lp:~chad.smith/lp2kanban/kanban-cards-in-feature-owners-next
Reviewer Review Type Date Requested Status
Benji York (community) Approve
Review via email: mp+281795@code.launchpad.net

Commit message

Allow feature tags on bugs to get automatically created in the designated squad todo_lane

Description of the change

bugs2cards will now observe the todo_lane setting from the lp2kanban sync.ini. It allows specific people/groups to be a feature owner.

When a feature card is on the LKK board assigned to a specific kanban user and has an unique "feature tag X", any LP bugs tagged with "kanban" and the "feature tag X" will be moved into the todo_lane instead of the default bug_to_card_lane.

In landscape's LKK, our configuration will look like this:
https://pastebin.canonical.com/146880/

By default, bugs tagged kanban in Launchpad will end up in Backlog::Engineering

If a feature card tagged "openstack-roles" is assigned to Alberto on our kanban board, then any new LP bugs tagged "kanban openstack-roles" will automatically be but into the "Development::Epsilon::Next" lane instead of "Backlog::Engineering".

To Test:
make configs
./create_creds # logs your local lp2kanban branch in as you so you can modify our production LKK board
# Select "Update all" option in your browser to give edit rights to your lp2kanban user

# Feel free to play around with Support taskboards card in Beta::Doing lane, there are cards already in landed that can automatically be pulled into the feature taskboard if you drag it into the Ready for QA lane
# There is also a TEST TAGGED CARD which has the same tag "lp2kanban" as the feature card above and depending on where you place the TEST card, it either will be pulled into the taskboard, or will be ignored.

# You can also create new fake bugs against landscape tagged with kanban and some known feature tag.

# Run lp2kanban to update the board. Watch the board's activity stream for updates from "LR" Landscape robot
./bin/py src/lp2kanban/bugs2cards.py -c configs/sync.ini -b 'Landscape 2016'

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. I, for one, welcome our new kanban overlords.

review: Approve
130. By Chad Smith

rewrap some docstrings per review.

Revision history for this message
Chad Smith (chad.smith) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lp2kanban/bugs2cards.py'
2--- src/lp2kanban/bugs2cards.py 2015-12-22 20:17:50 +0000
3+++ src/lp2kanban/bugs2cards.py 2016-01-08 16:41:46 +0000
4@@ -96,12 +96,12 @@
5 """Find Launchpad user accounts for board users and keep them cached.
6
7 Provides three dicts:
8- * lp_to_kanban which uses Launchpad account names as keys and points
9- to LeankitUser records.
10- * kanban_to_lp which uses LeankitUser records as keys pointing to
11- actual Launchpad user records.
12- * lp_to_group which uses Launchpad account names as keys and points
13- to group names.
14+ * lp_to_kanban which uses Launchpad account names as keys and points to
15+ LeankitUser records.
16+ * kanban_to_lp which uses LeankitUser.id as keys pointing to actual
17+ Launchpad user records.
18+ * lp_to_group which uses Launchpad account names as keys and points to
19+ group names.
20 """
21 def __init__(self, kanban_to_lp=None, lp_to_kanban=None, lp_to_group=None):
22 if kanban_to_lp is None:
23@@ -124,7 +124,7 @@
24 " user %s." % (user,))
25 continue
26 self.lp_to_kanban[lp_user.name] = board.users[user]
27- self.kanban_to_lp[board.users[user]] = lp_user
28+ self.kanban_to_lp[board.users[user].id] = lp_user
29 groups_config_file = config.get("groups_config_file")
30 if groups_config_file:
31 assert os.path.exists(groups_config_file), (
32@@ -133,6 +133,31 @@
33 groups_config.read(groups_config_file)
34 self.lp_to_group.update(parse_groups_config(groups_config))
35
36+ self.feature_lanes = {}
37+ todo_lane = config.get("todo_lane")
38+ if not todo_lane:
39+ return
40+ for card in board.getFeatureCards():
41+ if not card.tags:
42+ print ("Info: feature card '%s' has no tags. New bugs tagged "
43+ "for this feature will not be moved to the proper "
44+ "todo_lane." % card.title)
45+ continue
46+ if "${group}" in todo_lane:
47+ lp_user = self.kanban_to_lp.get(card.assigned_user_id)
48+ if not lp_user:
49+ print ("Info: feature card '%s' has no assigned user. New "
50+ "bugs tagged for this feature will not be moved to "
51+ "the proper todo_lane." % card.title)
52+ continue
53+ lane_path = todo_lane.replace(
54+ "${group}", self.lp_to_group[lp_user.name])
55+ else:
56+ lane_path = todo_lane
57+ for tag in card.tags.split(","):
58+ self.feature_lanes[tag.strip()] = board.getLaneByPath(
59+ lane_path)
60+
61
62 ORDERED_STATUSES = {'Rejected': 0,
63 'Work in progress': 0,
64@@ -209,7 +234,7 @@
65
66 @param feature_card: The feature LeankitCard to associate
67 @param feature_bug: The Launchpad bug resource linked to this feature_card
68- @param card_type: only return cards this type: MP, Feature, Task, ...
69+ @param card_type: only return cards this type: Branch, Feature, Task, ...
70 """
71 feature_tags = set()
72 linked_cards = set()
73@@ -477,8 +502,9 @@
74 return target_lane
75 return None
76
77+
78 def create_new_cards(board, launchpad_project, bug_tag, cardtype_name=None,
79- cardlane_path=None):
80+ cardlane_path=None, feature_lanes={}):
81 """Create cards for bugs tagged in Launchpad.
82
83 If a bug is tagged with the given tag, a new card will be created in
84@@ -499,8 +525,16 @@
85 while next_lane.child_lanes:
86 next_lane = next_lane.child_lanes[0]
87 for bug in kanban_bugs:
88- print " Creating card for bug %s in %s." % (bug.id, next_lane.title)
89- card = next_lane.addCard()
90+ feature_lane = None
91+ for tag in set(bug.tags).intersection(set(feature_lanes.keys())):
92+ if tag != 'no-sync':
93+ feature_lane = feature_lanes[tag]
94+ break
95+ if feature_lane:
96+ card = feature_lane.addCard()
97+ else:
98+ card = next_lane.addCard()
99+ print " Creating card for bug %s in %s." % (bug.id, card.lane.title)
100 card.title = bug.title
101 card.description = bug.description
102 card.external_card_id = str(bug.id)
103@@ -558,7 +592,7 @@
104 print " Creating new cards for %s:" % project.name
105 create_new_cards(
106 board, project, new_bug_tag, bconf.get("bug_to_card_type"),
107- bconf.get("bug_to_card_lane"))
108+ bconf.get("bug_to_card_lane"), lp_users.feature_lanes)
109 print " Syncing cards:"
110 # Process linked branch cards
111 for card in board.getCardsWithExternalLinks(only_branches=True):
112
113=== modified file 'src/lp2kanban/tests/common.py'
114--- src/lp2kanban/tests/common.py 2015-12-21 21:28:40 +0000
115+++ src/lp2kanban/tests/common.py 2016-01-08 16:41:46 +0000
116@@ -34,7 +34,10 @@
117 self._cards_with_description_annotations = set()
118 self._cards_with_external_links = set()
119 self._cards_with_branches = set()
120- self._feature_cards = set()
121+ if feature_cards is None:
122+ self._feature_cards = set()
123+ else:
124+ self._feature_cards = feature_cards
125 self.lanes = {}
126 self.root_lane = self.addLane('ROOT LANE')
127
128
129=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
130--- src/lp2kanban/tests/test_bugs2cards.py 2015-12-22 20:09:07 +0000
131+++ src/lp2kanban/tests/test_bugs2cards.py 2016-01-08 16:41:46 +0000
132@@ -664,13 +664,13 @@
133
134 def test_get_cards_for_feature_can_filter_by_card_type(self):
135 """Cards not matching card_type will not be returned."""
136- mp_card = FauxCard(tags="feature1")
137- mp_type = FauxCardType(id=2, name='MP', is_default=False)
138- mp_card.type = mp_type
139- self.board.cards.extend([mp_card])
140+ branch_card = FauxCard(tags="feature1")
141+ branch_type = FauxCardType(id=2, name='Branch', is_default=False)
142+ branch_card.type = branch_type
143+ self.board.cards.extend([branch_card])
144 cards = get_cards_for_feature(
145- self.feature_card, self.feature_bug, card_type='MP')
146- self.assertItemsEqual([mp_card], cards)
147+ self.feature_card, self.feature_bug, card_type='Branch')
148+ self.assertItemsEqual([branch_card], cards)
149
150
151 class FindLaneNextTest(unittest.TestCase):
152@@ -936,6 +936,11 @@
153
154 class LaunchpadUsersForBoardTest(unittest.TestCase):
155
156+ def setUp(self):
157+ self.branch_type = FauxCardType(id=1, name='Branch', is_default=False)
158+ self.feature_type = FauxCardType(
159+ id=2, name='Feature', is_default=False)
160+
161 def test_no_groups_config(self):
162 # If the config doesn't contain an entry for a groups config,
163 # ane empty dict is created for the group mapping.
164@@ -966,9 +971,99 @@
165 lp_users = LaunchpadUsersForBoard()
166 lp_users.set_up_users(lp, board, config)
167
168- self.assertEqual(
169- {"foo": "Group 1", "bar": "Group 1", "baz": "Group 2"},
170- lp_users.lp_to_group)
171+ def test_feature_lanes_unset_without_feature_cards(self):
172+ # When no feature cards exist on a board, the feature_lanes dictionary
173+ # is empty and todo_lane setting is not represented.
174+ board = FauxBoard(users=[], feature_cards=[])
175+ coding = board.addLane("dev::coding")
176+ card = coding.addCard(FauxCard(tags=["sometag"]))
177+ card.type = self.branch_type
178+ lp = None
179+ config = {"todo_lane": "my-Next-lane"}
180+ lp_users = LaunchpadUsersForBoard()
181+ lp_users.set_up_users(lp, board, config)
182+ self.assertEqual({}, lp_users.feature_lanes)
183+
184+ def test_feature_lanes_unset_without_todo_lane_config(self):
185+ # When feature cards are present, feature_lanes is empty when no
186+ # todo_lane setting is in the config.
187+ feature_card = FauxCard(tags=["sometag"])
188+ feature_card.type = self.feature_type
189+ board = FauxBoard(
190+ users=[], cards=[feature_card], feature_cards=[feature_card])
191+ coding_lane = board.addLane("dev::coding")
192+ coding_lane.addCard(feature_card)
193+ lp = None
194+ config = {"todo_lane": ""}
195+ lp_users = LaunchpadUsersForBoard()
196+ lp_users.set_up_users(lp, board, config)
197+ self.assertEqual({}, lp_users.feature_lanes)
198+
199+ def test_feature_lanes_set_with_feature_card_and_static_todo_lane(self):
200+ # When a feature card is present and tagged and the config contains
201+ # todo_lane which doesn't specify the ${group} variable, feature_lanes
202+ # will be set for all tags.
203+ feature_card = FauxCard(tags="sometag,anothertag")
204+ feature_card.type = self.feature_type
205+ board = FauxBoard(
206+ users=[], cards=[feature_card], feature_cards=[feature_card])
207+ coding_lane = board.addLane("dev::coding")
208+ coding_lane.addCard(feature_card)
209+ lp = None
210+ config = {"todo_lane": "dev::coding"}
211+ lp_users = LaunchpadUsersForBoard()
212+ lp_users.set_up_users(lp, board, config)
213+ self.assertEqual(
214+ {"sometag": coding_lane, "anothertag": coding_lane},
215+ lp_users.feature_lanes)
216+
217+ def test_feature_lanes_unset_with_group_todo_lane_and_no_assignee(self):
218+ # When a feature card is present and tagged and the todo_lane
219+ # in the config specifies the ${group} variable,
220+ # feature_lanes will be unset when there are no assigned users on the
221+ # card.
222+ feature_card = FauxCard(tags="sometag")
223+ feature_card.type = self.feature_type
224+ board = FauxBoard(
225+ users=[], cards=[feature_card], feature_cards=[feature_card])
226+ coding_lane = board.addLane("dev::group1::coding")
227+ coding_lane.addCard(feature_card)
228+ lp = None
229+ config = {"todo_lane": "dev::${group}::coding"}
230+ lp_users = LaunchpadUsersForBoard()
231+ lp_users.set_up_users(lp, board, config)
232+ self.assertIsNone(feature_card.assigned_user_id)
233+ self.assertEqual({}, lp_users.feature_lanes)
234+
235+ def test_feature_lanes_set_with_group_todo_lane_and_assignee(self):
236+ # When a feature card is present and tagged and the todo_lane
237+ # in the config specifies the ${group} variable,
238+ # feature_lanes will be set when there are is an assigned user on the
239+ # card.
240+ feature_card = FauxCard(tags="sometag,anothertag")
241+ feature_card.type = self.feature_type
242+ feature_card.assigned_user_id = 1
243+ groups_conf = '''
244+ [Group 1]
245+ members = User1,User2
246+ [Group 2]
247+ members = User3
248+ '''
249+ groups_file = tempfile.NamedTemporaryFile()
250+ groups_file.write(dedent(groups_conf))
251+ groups_file.flush()
252+ board = FauxBoard(
253+ users=[], cards=[feature_card], feature_cards=[feature_card])
254+ coding_lane = board.addLane("dev::Group 1::coding")
255+ coding_lane.addCard(feature_card)
256+ lp = None
257+ config = {"groups_config_file": groups_file.name,
258+ "todo_lane": "dev::${group}::coding"}
259+ lp_users = LaunchpadUsersForBoard(kanban_to_lp={1:FauxPerson("User1")})
260+ lp_users.set_up_users(lp, board, config)
261+ self.assertEqual(
262+ {"sometag": coding_lane, "anothertag": coding_lane},
263+ lp_users.feature_lanes)
264
265
266 class FauxPerson:
267@@ -985,7 +1080,7 @@
268 self.feature_bug = FauxBug(bug_id=123)
269 self.feature_bug.linked_branches = [
270 FauxBugBranch([], web_link=BRANCH_URL)]
271- self.mp_type = FauxCardType(id=1, name='MP', is_default=False)
272+ self.branch_type = FauxCardType(id=1, name='Branch', is_default=False)
273 self.feature_type = FauxCardType(
274 id=2, name='Feature', is_default=False)
275 self.feature_card.type = self.feature_type
276@@ -1010,7 +1105,7 @@
277 """
278 linked_card = FauxCard(external_system_url=BRANCH_URL)
279 linked_card.lane = self.coding
280- linked_card.type = self.mp_type
281+ linked_card.type = self.branch_type
282 self.board.cards.extend([linked_card])
283 move_cards_to_feature_taskboard(
284 self.feature_card, self.feature_bug, ["somelane", "otherlane"])
285@@ -1023,10 +1118,10 @@
286 """
287 linked_card = FauxCard(external_system_url=BRANCH_URL)
288 linked_card.lane = self.landing
289- linked_card.type = self.mp_type
290+ linked_card.type = self.branch_type
291 linked_card2 = FauxCard(external_system_url=BRANCH_URL)
292 linked_card2.lane = self.deploy
293- linked_card2.type = self.mp_type
294+ linked_card2.type = self.branch_type
295 self.board.cards.extend([linked_card, linked_card2])
296 move_cards_to_feature_taskboard(
297 self.feature_card, self.feature_bug, ["landing", "deploy"])
298@@ -1186,14 +1281,14 @@
299 class MoveCardTest(unittest.TestCase):
300
301 def setUp(self):
302- self.mp_type = FauxCardType(
303- id=1, name='MP', is_default=False)
304+ self.branch_type = FauxCardType(
305+ id=1, name='Branch', is_default=False)
306
307 def test_should_move_not_if_disabled(self):
308 board = FauxBoard(cards=[], is_archived=False)
309 lane = FauxLane(board=board)
310 card = FauxCard(lane=lane)
311- card.type = self.mp_type
312+ card.type = self.branch_type
313 board.cards.append(card)
314 self.assertFalse(should_move_card(card, {'move_cards': 'off'}))
315
316@@ -1201,7 +1296,7 @@
317 board = FauxBoard(cards=[], is_archived=False)
318 lane = FauxLane(board=board)
319 card = FauxCard(lane=lane)
320- card.type = self.mp_type
321+ card.type = self.branch_type
322 board.cards.append(card)
323 self.assertFalse(should_move_card(card, {}))
324
325@@ -1209,7 +1304,7 @@
326 board = FauxBoard(cards=[], is_archived=True)
327 lane = FauxLane(board=board)
328 card = FauxCard(lane=lane)
329- card.type = self.mp_type
330+ card.type = self.branch_type
331 board.cards.append(card)
332 self.assertFalse(should_move_card(card, {'move_cards': 'on'}))
333
334@@ -1217,7 +1312,7 @@
335 board = FauxBoard(cards=[], is_archived=False)
336 lane = FauxLane(board=board)
337 card = FauxCard(lane=lane)
338- card.type = self.mp_type
339+ card.type = self.branch_type
340 board.cards.append(card)
341 self.assertTrue(should_move_card(card, {'move_cards': 'on'}))
342

Subscribers

People subscribed via source and target branches

to all changes: