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
=== modified file 'src/lp2kanban/bugs2cards.py'
--- src/lp2kanban/bugs2cards.py 2015-12-22 20:17:50 +0000
+++ src/lp2kanban/bugs2cards.py 2016-01-04 22:39:12 +0000
@@ -98,7 +98,7 @@
98 Provides three dicts:98 Provides three dicts:
99 * lp_to_kanban which uses Launchpad account names as keys and points99 * lp_to_kanban which uses Launchpad account names as keys and points
100 to LeankitUser records.100 to LeankitUser records.
101 * kanban_to_lp which uses LeankitUser records as keys pointing to101 * kanban_to_lp which uses LeankitUser.id as keys pointing to
102 actual Launchpad user records.102 actual Launchpad user records.
103 * lp_to_group which uses Launchpad account names as keys and points103 * lp_to_group which uses Launchpad account names as keys and points
104 to group names.104 to group names.
@@ -124,7 +124,7 @@
124 " user %s." % (user,))124 " user %s." % (user,))
125 continue125 continue
126 self.lp_to_kanban[lp_user.name] = board.users[user]126 self.lp_to_kanban[lp_user.name] = board.users[user]
127 self.kanban_to_lp[board.users[user]] = lp_user127 self.kanban_to_lp[board.users[user].id] = lp_user
128 groups_config_file = config.get("groups_config_file")128 groups_config_file = config.get("groups_config_file")
129 if groups_config_file:129 if groups_config_file:
130 assert os.path.exists(groups_config_file), (130 assert os.path.exists(groups_config_file), (
@@ -133,6 +133,31 @@
133 groups_config.read(groups_config_file)133 groups_config.read(groups_config_file)
134 self.lp_to_group.update(parse_groups_config(groups_config))134 self.lp_to_group.update(parse_groups_config(groups_config))
135135
136 self.feature_lanes = {}
137 todo_lane = config.get("todo_lane")
138 if not todo_lane:
139 return
140 for card in board.getFeatureCards():
141 if not card.tags:
142 print ("Info: feature card '%s' has no tags. New bugs tagged "
143 "for this feature will not be moved to the proper "
144 "todo_lane." % card.title)
145 continue
146 if "${group}" in todo_lane:
147 lp_user = self.kanban_to_lp.get(card.assigned_user_id)
148 if not lp_user:
149 print ("Info: feature card '%s' has no assigned user. New "
150 "bugs tagged for this feature will not be moved to "
151 "the proper todo_lane." % card.title)
152 continue
153 lane_path = todo_lane.replace(
154 "${group}", self.lp_to_group[lp_user.name])
155 else:
156 lane_path = todo_lane
157 for tag in card.tags.split(","):
158 self.feature_lanes[tag.strip()] = board.getLaneByPath(
159 lane_path)
160
136161
137ORDERED_STATUSES = {'Rejected': 0,162ORDERED_STATUSES = {'Rejected': 0,
138 'Work in progress': 0,163 'Work in progress': 0,
@@ -209,7 +234,7 @@
209234
210 @param feature_card: The feature LeankitCard to associate235 @param feature_card: The feature LeankitCard to associate
211 @param feature_bug: The Launchpad bug resource linked to this feature_card236 @param feature_bug: The Launchpad bug resource linked to this feature_card
212 @param card_type: only return cards this type: MP, Feature, Task, ...237 @param card_type: only return cards this type: Branch, Feature, Task, ...
213 """238 """
214 feature_tags = set()239 feature_tags = set()
215 linked_cards = set()240 linked_cards = set()
@@ -477,8 +502,9 @@
477 return target_lane502 return target_lane
478 return None503 return None
479504
505
480def create_new_cards(board, launchpad_project, bug_tag, cardtype_name=None,506def create_new_cards(board, launchpad_project, bug_tag, cardtype_name=None,
481 cardlane_path=None):507 cardlane_path=None, feature_lanes={}):
482 """Create cards for bugs tagged in Launchpad.508 """Create cards for bugs tagged in Launchpad.
483509
484 If a bug is tagged with the given tag, a new card will be created in510 If a bug is tagged with the given tag, a new card will be created in
@@ -499,8 +525,16 @@
499 while next_lane.child_lanes:525 while next_lane.child_lanes:
500 next_lane = next_lane.child_lanes[0]526 next_lane = next_lane.child_lanes[0]
501 for bug in kanban_bugs:527 for bug in kanban_bugs:
502 print " Creating card for bug %s in %s." % (bug.id, next_lane.title)528 feature_lane = None
503 card = next_lane.addCard()529 for tag in set(bug.tags).intersection(set(feature_lanes.keys())):
530 if tag != 'no-sync':
531 feature_lane = feature_lanes[tag]
532 break
533 if feature_lane:
534 card = feature_lane.addCard()
535 else:
536 card = next_lane.addCard()
537 print " Creating card for bug %s in %s." % (bug.id, card.lane.title)
504 card.title = bug.title538 card.title = bug.title
505 card.description = bug.description539 card.description = bug.description
506 card.external_card_id = str(bug.id)540 card.external_card_id = str(bug.id)
@@ -558,7 +592,7 @@
558 print " Creating new cards for %s:" % project.name592 print " Creating new cards for %s:" % project.name
559 create_new_cards(593 create_new_cards(
560 board, project, new_bug_tag, bconf.get("bug_to_card_type"),594 board, project, new_bug_tag, bconf.get("bug_to_card_type"),
561 bconf.get("bug_to_card_lane"))595 bconf.get("bug_to_card_lane"), lp_users.feature_lanes)
562 print " Syncing cards:"596 print " Syncing cards:"
563 # Process linked branch cards597 # Process linked branch cards
564 for card in board.getCardsWithExternalLinks(only_branches=True):598 for card in board.getCardsWithExternalLinks(only_branches=True):
565599
=== modified file 'src/lp2kanban/tests/common.py'
--- src/lp2kanban/tests/common.py 2015-12-21 21:28:40 +0000
+++ src/lp2kanban/tests/common.py 2016-01-04 22:39:12 +0000
@@ -34,7 +34,10 @@
34 self._cards_with_description_annotations = set()34 self._cards_with_description_annotations = set()
35 self._cards_with_external_links = set()35 self._cards_with_external_links = set()
36 self._cards_with_branches = set()36 self._cards_with_branches = set()
37 self._feature_cards = set()37 if feature_cards is None:
38 self._feature_cards = set()
39 else:
40 self._feature_cards = feature_cards
38 self.lanes = {}41 self.lanes = {}
39 self.root_lane = self.addLane('ROOT LANE')42 self.root_lane = self.addLane('ROOT LANE')
4043
4144
=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
--- src/lp2kanban/tests/test_bugs2cards.py 2015-12-22 20:09:07 +0000
+++ src/lp2kanban/tests/test_bugs2cards.py 2016-01-04 22:39:12 +0000
@@ -664,13 +664,13 @@
664664
665 def test_get_cards_for_feature_can_filter_by_card_type(self):665 def test_get_cards_for_feature_can_filter_by_card_type(self):
666 """Cards not matching card_type will not be returned."""666 """Cards not matching card_type will not be returned."""
667 mp_card = FauxCard(tags="feature1")667 branch_card = FauxCard(tags="feature1")
668 mp_type = FauxCardType(id=2, name='MP', is_default=False)668 branch_type = FauxCardType(id=2, name='Branch', is_default=False)
669 mp_card.type = mp_type669 branch_card.type = branch_type
670 self.board.cards.extend([mp_card])670 self.board.cards.extend([branch_card])
671 cards = get_cards_for_feature(671 cards = get_cards_for_feature(
672 self.feature_card, self.feature_bug, card_type='MP')672 self.feature_card, self.feature_bug, card_type='Branch')
673 self.assertItemsEqual([mp_card], cards)673 self.assertItemsEqual([branch_card], cards)
674 674
675675
676class FindLaneNextTest(unittest.TestCase):676class FindLaneNextTest(unittest.TestCase):
@@ -936,6 +936,11 @@
936936
937class LaunchpadUsersForBoardTest(unittest.TestCase):937class LaunchpadUsersForBoardTest(unittest.TestCase):
938938
939 def setUp(self):
940 self.branch_type = FauxCardType(id=1, name='Branch', is_default=False)
941 self.feature_type = FauxCardType(
942 id=2, name='Feature', is_default=False)
943
939 def test_no_groups_config(self):944 def test_no_groups_config(self):
940 # If the config doesn't contain an entry for a groups config,945 # If the config doesn't contain an entry for a groups config,
941 # ane empty dict is created for the group mapping.946 # ane empty dict is created for the group mapping.
@@ -966,9 +971,99 @@
966 lp_users = LaunchpadUsersForBoard()971 lp_users = LaunchpadUsersForBoard()
967 lp_users.set_up_users(lp, board, config)972 lp_users.set_up_users(lp, board, config)
968973
969 self.assertEqual(974 def test_feature_lanes_unset_without_feature_cards(self):
970 {"foo": "Group 1", "bar": "Group 1", "baz": "Group 2"},975 # When no feature cards exist on a board, the feature_lanes dictionary
971 lp_users.lp_to_group)976 # is empty and todo_lane setting is not represented.
977 board = FauxBoard(users=[], feature_cards=[])
978 coding = board.addLane("dev::coding")
979 card = coding.addCard(FauxCard(tags=["sometag"]))
980 card.type = self.branch_type
981 lp = None
982 config = {"todo_lane": "my-Next-lane"}
983 lp_users = LaunchpadUsersForBoard()
984 lp_users.set_up_users(lp, board, config)
985 self.assertEqual({}, lp_users.feature_lanes)
986
987 def test_feature_lanes_unset_without_todo_lane_config(self):
988 # When feature cards are present, feature_lanes is empty when no
989 # todo_lane setting is in the config.
990 feature_card = FauxCard(tags=["sometag"])
991 feature_card.type = self.feature_type
992 board = FauxBoard(
993 users=[], cards=[feature_card], feature_cards=[feature_card])
994 coding_lane = board.addLane("dev::coding")
995 coding_lane.addCard(feature_card)
996 lp = None
997 config = {"todo_lane": ""}
998 lp_users = LaunchpadUsersForBoard()
999 lp_users.set_up_users(lp, board, config)
1000 self.assertEqual({}, lp_users.feature_lanes)
1001
1002 def test_feature_lanes_set_with_feature_card_and_static_todo_lane(self):
1003 # When a feature card is present and tagged and the config contains
1004 # todo_lane which doesn't specify the ${group} variable, feature_lanes
1005 # will be set for all tags.
1006 feature_card = FauxCard(tags="sometag,anothertag")
1007 feature_card.type = self.feature_type
1008 board = FauxBoard(
1009 users=[], cards=[feature_card], feature_cards=[feature_card])
1010 coding_lane = board.addLane("dev::coding")
1011 coding_lane.addCard(feature_card)
1012 lp = None
1013 config = {"todo_lane": "dev::coding"}
1014 lp_users = LaunchpadUsersForBoard()
1015 lp_users.set_up_users(lp, board, config)
1016 self.assertEqual(
1017 {"sometag": coding_lane, "anothertag": coding_lane},
1018 lp_users.feature_lanes)
1019
1020 def test_feature_lanes_unset_with_group_todo_lane_and_no_assignee(self):
1021 # When a feature card is present and tagged and the todo_lane
1022 # in the config specifies the ${group} variable,
1023 # feature_lanes will be unset when there are no assigned users on the
1024 # card.
1025 feature_card = FauxCard(tags="sometag")
1026 feature_card.type = self.feature_type
1027 board = FauxBoard(
1028 users=[], cards=[feature_card], feature_cards=[feature_card])
1029 coding_lane = board.addLane("dev::group1::coding")
1030 coding_lane.addCard(feature_card)
1031 lp = None
1032 config = {"todo_lane": "dev::${group}::coding"}
1033 lp_users = LaunchpadUsersForBoard()
1034 lp_users.set_up_users(lp, board, config)
1035 self.assertIsNone(feature_card.assigned_user_id)
1036 self.assertEqual({}, lp_users.feature_lanes)
1037
1038 def test_feature_lanes_set_with_group_todo_lane_and_assignee(self):
1039 # When a feature card is present and tagged and the todo_lane
1040 # in the config specifies the ${group} variable,
1041 # feature_lanes will be set when there are is an assigned user on the
1042 # card.
1043 feature_card = FauxCard(tags="sometag,anothertag")
1044 feature_card.type = self.feature_type
1045 feature_card.assigned_user_id = 1
1046 groups_conf = '''
1047 [Group 1]
1048 members = User1,User2
1049 [Group 2]
1050 members = User3
1051 '''
1052 groups_file = tempfile.NamedTemporaryFile()
1053 groups_file.write(dedent(groups_conf))
1054 groups_file.flush()
1055 board = FauxBoard(
1056 users=[], cards=[feature_card], feature_cards=[feature_card])
1057 coding_lane = board.addLane("dev::Group 1::coding")
1058 coding_lane.addCard(feature_card)
1059 lp = None
1060 config = {"groups_config_file": groups_file.name,
1061 "todo_lane": "dev::${group}::coding"}
1062 lp_users = LaunchpadUsersForBoard(kanban_to_lp={1:FauxPerson("User1")})
1063 lp_users.set_up_users(lp, board, config)
1064 self.assertEqual(
1065 {"sometag": coding_lane, "anothertag": coding_lane},
1066 lp_users.feature_lanes)
9721067
9731068
974class FauxPerson:1069class FauxPerson:
@@ -985,7 +1080,7 @@
985 self.feature_bug = FauxBug(bug_id=123)1080 self.feature_bug = FauxBug(bug_id=123)
986 self.feature_bug.linked_branches = [1081 self.feature_bug.linked_branches = [
987 FauxBugBranch([], web_link=BRANCH_URL)]1082 FauxBugBranch([], web_link=BRANCH_URL)]
988 self.mp_type = FauxCardType(id=1, name='MP', is_default=False)1083 self.branch_type = FauxCardType(id=1, name='Branch', is_default=False)
989 self.feature_type = FauxCardType(1084 self.feature_type = FauxCardType(
990 id=2, name='Feature', is_default=False)1085 id=2, name='Feature', is_default=False)
991 self.feature_card.type = self.feature_type1086 self.feature_card.type = self.feature_type
@@ -1010,7 +1105,7 @@
1010 """1105 """
1011 linked_card = FauxCard(external_system_url=BRANCH_URL)1106 linked_card = FauxCard(external_system_url=BRANCH_URL)
1012 linked_card.lane = self.coding1107 linked_card.lane = self.coding
1013 linked_card.type = self.mp_type1108 linked_card.type = self.branch_type
1014 self.board.cards.extend([linked_card])1109 self.board.cards.extend([linked_card])
1015 move_cards_to_feature_taskboard(1110 move_cards_to_feature_taskboard(
1016 self.feature_card, self.feature_bug, ["somelane", "otherlane"])1111 self.feature_card, self.feature_bug, ["somelane", "otherlane"])
@@ -1023,10 +1118,10 @@
1023 """1118 """
1024 linked_card = FauxCard(external_system_url=BRANCH_URL)1119 linked_card = FauxCard(external_system_url=BRANCH_URL)
1025 linked_card.lane = self.landing1120 linked_card.lane = self.landing
1026 linked_card.type = self.mp_type1121 linked_card.type = self.branch_type
1027 linked_card2 = FauxCard(external_system_url=BRANCH_URL)1122 linked_card2 = FauxCard(external_system_url=BRANCH_URL)
1028 linked_card2.lane = self.deploy1123 linked_card2.lane = self.deploy
1029 linked_card2.type = self.mp_type1124 linked_card2.type = self.branch_type
1030 self.board.cards.extend([linked_card, linked_card2])1125 self.board.cards.extend([linked_card, linked_card2])
1031 move_cards_to_feature_taskboard(1126 move_cards_to_feature_taskboard(
1032 self.feature_card, self.feature_bug, ["landing", "deploy"])1127 self.feature_card, self.feature_bug, ["landing", "deploy"])
@@ -1186,14 +1281,14 @@
1186class MoveCardTest(unittest.TestCase):1281class MoveCardTest(unittest.TestCase):
11871282
1188 def setUp(self):1283 def setUp(self):
1189 self.mp_type = FauxCardType(1284 self.branch_type = FauxCardType(
1190 id=1, name='MP', is_default=False)1285 id=1, name='Branch', is_default=False)
11911286
1192 def test_should_move_not_if_disabled(self):1287 def test_should_move_not_if_disabled(self):
1193 board = FauxBoard(cards=[], is_archived=False)1288 board = FauxBoard(cards=[], is_archived=False)
1194 lane = FauxLane(board=board)1289 lane = FauxLane(board=board)
1195 card = FauxCard(lane=lane)1290 card = FauxCard(lane=lane)
1196 card.type = self.mp_type1291 card.type = self.branch_type
1197 board.cards.append(card)1292 board.cards.append(card)
1198 self.assertFalse(should_move_card(card, {'move_cards': 'off'}))1293 self.assertFalse(should_move_card(card, {'move_cards': 'off'}))
11991294
@@ -1201,7 +1296,7 @@
1201 board = FauxBoard(cards=[], is_archived=False)1296 board = FauxBoard(cards=[], is_archived=False)
1202 lane = FauxLane(board=board)1297 lane = FauxLane(board=board)
1203 card = FauxCard(lane=lane)1298 card = FauxCard(lane=lane)
1204 card.type = self.mp_type1299 card.type = self.branch_type
1205 board.cards.append(card)1300 board.cards.append(card)
1206 self.assertFalse(should_move_card(card, {}))1301 self.assertFalse(should_move_card(card, {}))
12071302
@@ -1209,7 +1304,7 @@
1209 board = FauxBoard(cards=[], is_archived=True)1304 board = FauxBoard(cards=[], is_archived=True)
1210 lane = FauxLane(board=board)1305 lane = FauxLane(board=board)
1211 card = FauxCard(lane=lane)1306 card = FauxCard(lane=lane)
1212 card.type = self.mp_type1307 card.type = self.branch_type
1213 board.cards.append(card)1308 board.cards.append(card)
1214 self.assertFalse(should_move_card(card, {'move_cards': 'on'}))1309 self.assertFalse(should_move_card(card, {'move_cards': 'on'}))
12151310
@@ -1217,7 +1312,7 @@
1217 board = FauxBoard(cards=[], is_archived=False)1312 board = FauxBoard(cards=[], is_archived=False)
1218 lane = FauxLane(board=board)1313 lane = FauxLane(board=board)
1219 card = FauxCard(lane=lane)1314 card = FauxCard(lane=lane)
1220 card.type = self.mp_type1315 card.type = self.branch_type
1221 board.cards.append(card)1316 board.cards.append(card)
1222 self.assertTrue(should_move_card(card, {'move_cards': 'on'}))1317 self.assertTrue(should_move_card(card, {'move_cards': 'on'}))
12231318

Subscribers

People subscribed via source and target branches

to all changes: