Merge lp:~dpb/lp2kanban/feature-hoover-just-the-facts into lp:lp2kanban

Proposed by David Britton
Status: Merged
Approved by: David Britton
Approved revision: 151
Merged at revision: 145
Proposed branch: lp:~dpb/lp2kanban/feature-hoover-just-the-facts
Merge into: lp:lp2kanban
Diff against target: 318 lines (+92/-48)
4 files modified
src/lp2kanban/bugs2cards.py (+23/-20)
src/lp2kanban/kanban.py (+41/-11)
src/lp2kanban/tests/common.py (+6/-0)
src/lp2kanban/tests/test_bugs2cards.py (+22/-17)
To merge this branch: bzr merge lp:~dpb/lp2kanban/feature-hoover-just-the-facts
Reviewer Review Type Date Requested Status
Adam Collard (community) Approve
🤖 Landscape Builder test results Approve
Review via email: mp+305268@code.launchpad.net

Commit message

Change card consolidation logic to pull cards in as soon as they hit a 'consolidation_lane' in the configuration. Also, when consolidating to a taskboard, move to the 'Done' lane instead of the default "Todo" lane (Request from QA).

- various logging changes to help diagnose program flow
- Added method 'refreshTaskBoard' to pull in card changes between moves
- Added method 'moveInsideTaskBoard' to allow lane selection inside the taskboard
- test fixes to compensate for changes

Description of the change

Change card consolidation logic to pull cards in as soon as they hit a 'consolidation_lane' in the configuration. Also, when consolidating to a taskboard, move to the 'Done' lane instead of the default "Todo" lane (Request from QA).

- various logging changes to help diagnose program flow
- Added method 'refreshTaskBoard' to pull in card changes between moves
- Added method 'moveInsideTaskBoard' to allow lane selection inside the taskboard
- test fixes to compensate for changes

To test:

unit tests: see README

end-to-end test:

after getting your venv sorted, you can set up a dummy branch card in the 'landed' lane (or drag an old one out from a taskboard) and run the following:

  make configs
  make credentials
  ./jenkins.sh sync --card <card_id> --debug

* card_id comes from the URL when viewing the card.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$CI_PROXY https_proxy=$CI_PROXY make ci-test
Result: Success
Revno: 150
Branch: lp:~davidpbritton/lp2kanban/feature-hoover-just-the-facts
Jenkins: https://ci.lscape.net/job/latch-test/10189/

review: Approve (test results)
Revision history for this message
Adam Collard (adam-collard) wrote :

Minor nit pick. +1

review: Approve
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new 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 2016-09-05 02:37:53 +0000
3+++ src/lp2kanban/bugs2cards.py 2016-09-09 15:09:18 +0000
4@@ -231,14 +231,16 @@
5 linked_cards = get_cards_for_feature(feature_card, feature_bug=feature_bug)
6 for card in linked_cards:
7 if filter_card and filter_card != card.id:
8- return
9+ continue
10 if card.type.name == "Feature": # We don't want to move feature cards
11- continue
12+ continue
13 card_path = card.lane.path
14 if card_path in source_lanes:
15- log.info(u" * Moving to taskboard '{}' card-{}".format(
16- feature_card.title, card.title))
17+ log.info(u" * Moving card: {}/{} to taskboard: {}/{}".format(
18+ card.id, card.title, feature_card.id, feature_card.title))
19 card.moveToTaskBoard(feature_card)
20+ feature_card.refreshTaskBoard()
21+ card.moveInsideTaskBoard(feature_card, 'Done')
22 else:
23 log.debug(
24 "Card in progress, not moving to taskboard '%s' card-%s." % (
25@@ -639,7 +641,7 @@
26 'lp2kanban', config['lp_server'], version="devel",
27 credentials_file=credentials_file,
28 launchpadlib_dir=launchpadlib_dir)
29- log.info("Logged in as", lp.me.name)
30+ log.info(u"Logged in as {}".format(lp.me.name))
31 else:
32 lp = Launchpad.login_anonymously(
33 'lp2kanban', config['lp_server'], version="devel",
34@@ -792,24 +794,25 @@
35 board, bconf, lp, lp_users, filter_card=None):
36 """Move branch cards to feature cards if they are landed."""
37 log.info("Move cards to taskboards")
38- if "${group}" in bconf["landing_lanes"]:
39+ valid_lanes = []
40+ consolidation_lanes = bconf.get("consolidation_lanes", "")
41+ if "${group}" in consolidation_lanes:
42 # Replace the optional template ${group} with configured board groups.
43- # This creates a list of landing_lanes, one per group on the board.
44- valid_taskboard_lanes = [
45- bconf["landing_lanes"].replace("${group}", group)
46- for group in set(lp_users.lp_to_group.values())]
47+ # This creates a list of lanes, one per group on the board.
48+ lanes = consolidation_lanes.split(",")
49+ for lane in lanes:
50+ for group in set(lp_users.lp_to_group.values()):
51+ valid_lanes.append(lane.replace("${group}", group))
52 else:
53- valid_taskboard_lanes = bconf["landing_lanes"]
54+ valid_lanes = consolidation_lanes.split(",")
55
56 for feature_card in board.getFeatureCards():
57- if (feature_card.lane.path in bconf["deploy_lanes"] or
58- feature_card.lane.path in valid_taskboard_lanes):
59- lp_bug = get_lp_bug(feature_card, lp)
60- move_cards_to_feature_taskboard(
61- feature_card, lp_bug, valid_taskboard_lanes, filter_card)
62-
63-
64-def convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects):
65+ lp_bug = get_lp_bug(feature_card, lp)
66+ move_cards_to_feature_taskboard(
67+ feature_card, lp_bug, valid_lanes, filter_card)
68+
69+
70+def cards_to_work_items(board, bconf, lp, lp_users, lp_projects):
71 """Create work items out of cards if desired."""
72 log.info("Convert cards to work items")
73 if bconf.get("convert_cards_to_work_items", False) == "on":
74@@ -922,7 +925,7 @@
75 move_cards_to_taskboards(
76 board, bconf, lp, lp_users, filter_card=filter_card)
77 if not filter_bug and not filter_card:
78- convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects)
79+ cards_to_work_items(board, bconf, lp, lp_users, lp_projects)
80 save_board(board)
81
82
83
84=== modified file 'src/lp2kanban/kanban.py'
85--- src/lp2kanban/kanban.py 2016-09-01 18:55:23 +0000
86+++ src/lp2kanban/kanban.py 2016-09-09 15:09:18 +0000
87@@ -191,9 +191,11 @@
88 class LeankitUser(Converter):
89 attributes = ['UserName', 'FullName', 'EmailAddress', 'Id']
90
91+
92 class LeankitCardType(Converter):
93 attributes = ['Name', 'IsDefault', 'ColorHex', 'IconPath', 'Id']
94
95+
96 class LeankitCard(Converter):
97 attributes = ['Id', 'Title', 'Priority', 'Description', 'Tags',
98 'TypeId']
99@@ -287,13 +289,27 @@
100 else:
101 return None
102
103- def moveToTaskBoard(self, target_card):
104- """Move the current card into a subtask of target_card."""
105+ def refreshTaskBoard(self):
106+ """Refresh taskboard data for card (if any exists)."""
107+ taskboard_data = self.lane.board.connector.get(
108+ "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (
109+ self.lane.board.id, self.id))
110+ self.taskboard = LeankitTaskBoard(
111+ taskboard_data["ReplyData"][0], self.lane.board, self)
112+ self.taskboard.fetchDetails()
113+
114+ def moveToTaskBoard(self, target_card, target_lane=None):
115+ """Move the current card into a subtask of target_card.
116+
117+ If target_lane specified, put the card into a specific
118+ lane on the taskboard (default = taskboard default).
119+ """
120 url = '/Api/Card/MoveCardToTaskboard'
121 result = self.lane.board.connector.post(url, data={
122 'boardId': self.lane.board.id,
123 'destCardId': target_card.id,
124 'srcCardId': [self.id]})
125+
126 if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
127 return result.ReplyData[0]
128 else:
129@@ -302,6 +318,21 @@
130 self.title, self.id, self.lane.path) +
131 "Error %s: %s" % (result.ReplyCode, result.ReplyText))
132
133+ def moveInsideTaskBoard(self, taskboard_card, target_lane_name):
134+ """Move this card to a lane inside a taskboard."""
135+ target_lane = taskboard_card.taskboard.getLaneByTitle(target_lane_name)
136+ url = '/kanban/api/v1/board/%d/move/card/%d/tasks/%d/lane/%d' % (
137+ taskboard_card.lane.board.id, taskboard_card.id, self.id,
138+ target_lane.id)
139+ result = self.lane.board.connector.post(url, data=None)
140+ if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
141+ return result.ReplyData[0]
142+ else:
143+ raise Exception(
144+ "Moving card: %s (%s) inside taskboard: %s/%s failed. " % (
145+ self.title, self.id, taskboard_card.id, target_lane_name) +
146+ "Error %s: %s" % (result.ReplyCode, result.ReplyText))
147+
148 def _moveCard(self):
149 target_pos = len(self.lane.cards)
150 url = '/Kanban/Api/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % (
151@@ -534,7 +565,7 @@
152 def fetchDetails(self, include_archive=False, include_taskboards=False):
153 """Fetch all details from the board into our datastructure.
154
155- include_taskboards - add all cards on taskboards into my card list
156+ include_taskboards - fill out taskboard data structures
157 include_archive - add all cards from archive into my card list.
158 """
159 self.details = self.connector.get(
160@@ -557,12 +588,7 @@
161 if include_taskboards:
162 for card in self.cards:
163 if card.current_task_board_id: # We are a task board
164- taskboard_data = self.connector.get(
165- "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
166- card.taskboard = LeankitTaskBoard(
167- taskboard_data["ReplyData"][0], self, card)
168- card.taskboard.fetchDetails()
169- self.cards.extend(card.taskboard.cards)
170+ card.refreshTaskBoard()
171 self._classifyCards()
172 self._populateFeatureTagPaths()
173
174@@ -777,10 +803,14 @@
175 board = self._findBoardInCache(board_id, title)
176 return board
177
178- def getBoard(self, board_id=None, title=None, include_archive=False):
179+ def getBoard(
180+ self, board_id=None, title=None, include_archive=False,
181+ include_taskboards=True):
182 board = self._findBoard(board_id, title)
183 if board is not None:
184- board.fetchDetails(include_archive=include_archive)
185+ board.fetchDetails(
186+ include_archive=include_archive,
187+ include_taskboards=include_taskboards)
188 return board
189
190
191
192=== modified file 'src/lp2kanban/tests/common.py'
193--- src/lp2kanban/tests/common.py 2016-09-01 17:18:18 +0000
194+++ src/lp2kanban/tests/common.py 2016-09-09 15:09:18 +0000
195@@ -139,6 +139,12 @@
196 def moveToTaskBoard(self, target_card):
197 self.moved_to = target_card
198
199+ def refreshTaskBoard(self):
200+ pass
201+
202+ def moveInsideTaskBoard(self, taskboard_card, lane_name):
203+ self.moved_to = (taskboard_card, lane_name)
204+
205
206 class FauxLeankitUser:
207
208
209=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
210--- src/lp2kanban/tests/test_bugs2cards.py 2016-09-01 21:40:36 +0000
211+++ src/lp2kanban/tests/test_bugs2cards.py 2016-09-09 15:09:18 +0000
212@@ -1403,8 +1403,8 @@
213 self.board.cards.extend([linked_card, linked_card2])
214 move_cards_to_feature_taskboard(
215 self.feature_card, self.feature_bug, ["landing", "deploy"])
216- self.assertEqual(self.feature_card, linked_card.moved_to)
217- self.assertEqual(self.feature_card, linked_card2.moved_to)
218+ self.assertEqual((self.feature_card, 'Done'), linked_card.moved_to)
219+ self.assertEqual((self.feature_card, 'Done'), linked_card2.moved_to)
220
221
222 class MoveCardOmnidirectionalTest(unittest.TestCase):
223@@ -1630,11 +1630,12 @@
224 board.cards.append(card)
225 self.assertFalse(should_move_card(card, {'move_cards': 'on'}))
226
227+
228 class SyncBoardTest(unittest.TestCase):
229
230 def setUp(self):
231 """Create a board with a feature feature card with tag feature1"""
232- feature_type = FauxCardType(id=2, name='Feature1', is_default=False)
233+ feature_type = FauxCardType(id=2, name='Feature', is_default=False)
234 self.branch_type = FauxCardType(id=1, name='Branch', is_default=False)
235 self.feature_card = FauxCard(tags='feature1', title='Feature1')
236 self.feature_card.type = feature_type
237@@ -1643,10 +1644,13 @@
238 self.bconf = {
239 'landing_lanes': 'landing',
240 'deploy_lanes': 'deploy',
241+ 'consolidation_lanes': 'deploy,landing',
242 }
243 self.coding_lane = FauxLane(board=self.board, path='coding')
244 self.landing_lane = FauxLane(board=self.board, path='landing')
245 self.deploy_lane = FauxLane(board=self.board, path='deploy')
246+ self.consolidation_lanes = FauxLane(
247+ board=self.board, path='deploy,landing')
248 self.lp_users = FauxLaunchpadUsersForBoard()
249
250 def test_sync_cards_moves_landing_cards_to_taskboard_in_landing_lane(self):
251@@ -1665,14 +1669,14 @@
252 move_cards_to_taskboards(
253 self.board, self.bconf, lp=FauxLP(), lp_users=self.lp_users)
254 self.assertIsNone(coding_card.moved_to)
255- self.assertEqual(self.feature_card, landed_card.moved_to)
256+ self.assertEqual((self.feature_card, 'Done'), landed_card.moved_to)
257
258 def test_sync_cards_moves_landing_cards_to_taskboard_in_deploy_lane(self):
259- """
260- Feature-linked cards in landing lanes will move to taskboard when the
261- feature card is in deploy_lanes.
262- """
263- self.feature_card.lane = self.deploy_lane
264+ """
265+ Landed feature-linked cards will move to taskboard when the
266+ feature card is in consolidation_lanes.
267+ """
268+ self.feature_card.lane = self.consolidation_lanes
269 coding_card = FauxCard(tags='feature1', title='CodingCard')
270 coding_card.type = self.branch_type
271 coding_card.lane = self.coding_lane
272@@ -1683,11 +1687,11 @@
273 move_cards_to_taskboards(
274 self.board, self.bconf, lp=FauxLP(), lp_users=self.lp_users)
275 self.assertIsNone(coding_card.moved_to)
276- self.assertEqual(self.feature_card, landed_card.moved_to)
277+ self.assertEqual((self.feature_card, 'Done'), landed_card.moved_to)
278
279 def test_sync_cards_moves_no_cards_to_taskboard_not_landed_or_deploy(self):
280- """
281- Feature-linked cards in landing lanes will not move to taskboard when
282+ """
283+ Feature-linked cards in landing lanes will move to taskboard even when
284 the feature card is not in the landing_lanes or deploy_lanes.
285 """
286 self.feature_card.lane = self.coding_lane
287@@ -1697,16 +1701,17 @@
288 self.board.cards.extend([landed_card])
289 move_cards_to_taskboards(
290 self.board, self.bconf, lp=FauxLP(), lp_users=self.lp_users)
291- self.assertIsNone(landed_card.moved_to)
292+ self.assertEqual((self.feature_card, 'Done'), landed_card.moved_to)
293
294 def test_sync_cards_moves_landing_cards_to_taskboard_with_groups(self):
295- """
296- When landing_lanes contains ${groups} template, feature-linkd cards in
297+ """
298+ When landing_lanes contains ${groups} template, feature-linked cards in
299 all configured landing lanes will be moved to the feature taskboard.
300 """
301 self.bconf = {
302 'landing_lanes': '${group}::landing',
303 'deploy_lanes': 'deploy',
304+ 'consolidation_lanes': '${group}::landing,deploy',
305 }
306 landing_lane1 = FauxLane(board=self.board, path='ALPHA::landing')
307 landing_lane2 = FauxLane(board=self.board, path='BETA::landing')
308@@ -1726,8 +1731,8 @@
309 move_cards_to_taskboards(
310 self.board, self.bconf, lp=FauxLP(), lp_users=lp_users)
311 self.assertIsNone(coding_card.moved_to)
312- self.assertEqual(self.feature_card, landed_card1.moved_to)
313- self.assertEqual(self.feature_card, landed_card2.moved_to)
314+ self.assertEqual((self.feature_card, 'Done'), landed_card1.moved_to)
315+ self.assertEqual((self.feature_card, 'Done'), landed_card2.moved_to)
316
317 def test_sync_cards_external_ids_filter_card(self):
318 """filter_card skips and selects correctly."""

Subscribers

People subscribed via source and target branches

to all changes: