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

Proposed by Chad Smith
Status: Superseded
Proposed branch: lp:~chad.smith/lp2kanban/kanban-cards-in-feature-owners-next
Merge into: lp:lp2kanban
Diff against target: 331 lines (+159/-27)
3 files modified
src/lp2kanban/bugs2cards.py (+41/-7)
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
Landscape Pending
Review via email: mp+281557@code.launchpad.net

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.
128. By Chad Smith

revert buildout test change

129. By Chad Smith

change unit tests to validate 'Branch card types instead of MP'

130. By Chad Smith

rewrap some docstrings per review.

Unmerged revisions

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

Subscribers

People subscribed via source and target branches

to all changes: