Merge lp:~chad.smith/lp2kanban/branch-to-card into lp:lp2kanban
- branch-to-card
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 120 |
Proposed branch: | lp:~chad.smith/lp2kanban/branch-to-card |
Merge into: | lp:lp2kanban |
Diff against target: |
554 lines (+213/-46) 4 files modified
src/lp2kanban/bugs2cards.py (+70/-13) src/lp2kanban/kanban.py (+79/-16) src/lp2kanban/tests/common.py (+5/-2) src/lp2kanban/tests/test_bugs2cards.py (+59/-15) |
To merge this branch: | bzr merge lp:~chad.smith/lp2kanban/branch-to-card |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Данило Шеган (community) | Approve | ||
Landscape | Pending | ||
Review via email: mp+280729@code.launchpad.net |
Commit message
Description of the change
Add initial support for branch cards and taskboards. This branch only handles syncing and moving branch "MP/blue" cards on the Landscape 2016 board.
Support was added for branch cards inside bugs2card because a lot of the logic is common and can hopefully be consolidated in a subsequent branch.
There will be a followup branch with added logic in bugs2cards to call moveToTaskBoard for feature cards that are manually dragged to the "$Group::Landed" lane.
src/lp2kanban/
- Define initial LeankitTaskBoard class
- Add optional CurrentTaskBoardId property to LeankitCard
- Add LeankitCard.
- Since Taskboard API calls live on a different route, rework API routes defined on the LeankitConnector class
src/lp2kanban/
- _get_mp_info to handle non-bug branch records when parsing linked MPs
- should_sync_card to return true if external_system_url is a branch
- get_card_status returns proper status for cards that have external_
- add a loop to sync_board function which processes all "branch cards" through board.getCardsW
config/sync.ini:
- comment out [Landscape Cisco] board definition (as that should live in this board now
To test:
buildout
./bin/test
# to test against the new landscape 2016 board
./create_creds.py (this will create your creds to modify the shared 2016 board)
Select "Change anything" button in your browser to allow updating cards
make config
./bin/py src/lp2kanban/
# Tweak branches or drag cards at https:/
What's lacking:
unit tests to cover bugs2card sync_board (coming in a followup branch)
- 135. By Chad Smith
-
simplify moveToTaskBoard card method
- 136. By Chad Smith
-
merge ~landscape/
lp2kanban/ landscape- deploy to drop configs
- 137. By Chad Smith
-
address review comments: approved branches that have not yet been merged will remain in the status LANDING. update copyright.
- 138. By Chad Smith
-
fix has_branch conditional to first check if card.external_
system_ url is not None before trying to match BRANCH_REGEX - 139. By Chad Smith
-
card.save needs to be taskboard aware. When calling UpdateCard the parent_board.id needs to be specified, not the taskboard.id
- 140. By Chad Smith
-
add parent_card param to LeankitTaskBoard so that we have a refernce to the taskboard's card on the parent_board. Use the parent_card for card path printing
- 141. By Chad Smith
-
merge landscape-deploy to get latest changes to jenkins.sh
- 142. By Chad Smith
-
revert test makefile change
Preview Diff
1 | === modified file 'src/lp2kanban/bugs2cards.py' |
2 | --- src/lp2kanban/bugs2cards.py 2015-12-04 15:59:31 +0000 |
3 | +++ src/lp2kanban/bugs2cards.py 2015-12-17 00:04:21 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -# Copyright 2011 Canonical Ltd |
6 | +# Copyright 2011-2015 Canonical Ltd |
7 | # |
8 | from ConfigParser import ConfigParser |
9 | from argparse import ArgumentParser |
10 | @@ -9,7 +9,9 @@ |
11 | update_blueprints_from_work_items, |
12 | ) |
13 | from lp2kanban.kanban import ( |
14 | + BRANCH_REGEX, |
15 | LeankitKanban, |
16 | + LeankitTaskBoard, |
17 | Record, |
18 | ) |
19 | import os.path |
20 | @@ -164,8 +166,10 @@ |
21 | |
22 | def _get_mp_info(): |
23 | mps = [] |
24 | - for bug_branch in branches: |
25 | - for mp in bug_branch.branch.landing_targets: |
26 | + for branch in branches: |
27 | + if hasattr(branch, 'branch') : |
28 | + branch = branch.branch |
29 | + for mp in branch.landing_targets: |
30 | mp_info = Record(rank=None, status=None, mp=None) |
31 | status = mp.queue_status |
32 | mp_info.rank = ORDERED_STATUSES.get(status, 1) |
33 | @@ -225,8 +229,11 @@ |
34 | if conf.get('autosync', None) == 'on': |
35 | has_id = (card.external_card_id is not None and |
36 | card.external_card_id.strip() != '') |
37 | - return (has_id and NO_SYNC_MARKER not in card.title and |
38 | - NO_SYNC_MARKER not in card.description) |
39 | + has_branch = (card.external_system_url is not None and |
40 | + BRANCH_REGEX.match(card.external_system_url)) |
41 | + if NO_SYNC_MARKER in card.title or NO_SYNC_MARKER in card.description: |
42 | + return False |
43 | + return (has_id or has_branch) |
44 | else: |
45 | return (card.title.startswith(TITLE_MARKER) or |
46 | DESCRIPTION_MARKER in card.description) |
47 | @@ -299,15 +306,21 @@ |
48 | """Return the status of the card as one of CardStatus values.""" |
49 | # status of None means card needs not to be moved. |
50 | status = None |
51 | - if bug_status in IN_PROGRESS_BUG_STATUSES: |
52 | - status = CardStatus.CODING |
53 | + branch_card_status = None |
54 | + if branch_info: |
55 | if branch_info.status == 'In Progress': |
56 | - status = CardStatus.CODING |
57 | + branch_card_status = CardStatus.CODING |
58 | elif branch_info.status == 'In Review': |
59 | - status = CardStatus.REVIEW |
60 | + branch_card_status = CardStatus.REVIEW |
61 | elif branch_info.status == 'Approved': |
62 | - status = CardStatus.LANDING |
63 | + branch_card_status = CardStatus.REVIEW |
64 | elif branch_info.status == 'Merged': |
65 | + branch_card_status = CardStatus.LANDING |
66 | + if bug_status is None: |
67 | + return branch_card_status |
68 | + if bug_status in IN_PROGRESS_BUG_STATUSES: |
69 | + status = CardStatus.CODING |
70 | + if branch_info.status == 'Merged': |
71 | if bug_status == 'Fix Committed': |
72 | if 'qa-needstesting' in bug_tags: |
73 | status = CardStatus.QA |
74 | @@ -318,6 +331,8 @@ |
75 | status = CardStatus.DEPLOY |
76 | else: |
77 | status = CardStatus.LANDING |
78 | + elif branch_card_status: |
79 | + return branch_card_status |
80 | elif bug_status in DONE_BUG_STATUSES: |
81 | status = CardStatus.DONE |
82 | return status |
83 | @@ -457,6 +472,43 @@ |
84 | board, project, new_bug_tag, bconf.get("bug_to_card_type"), |
85 | bconf.get("bug_to_card_lane")) |
86 | print " Syncing cards:" |
87 | + # Process linked branch cards |
88 | + for card in board.getCardsWithExternalLinks(only_branches=True): |
89 | + if card.external_card_id: |
90 | + # Cards with external_id are processed by getCardsWithExternalIds |
91 | + continue |
92 | + branch_name = card.external_system_url.replace( |
93 | + "https://code.launchpad.net/", "") |
94 | + branch = lp.branches.getByUniqueName(unique_name=branch_name) |
95 | + if not branch: |
96 | + print "Invalid branch url ({}) for card '{}'".format( |
97 | + card.external_system_url, card.title) |
98 | + continue |
99 | + if should_sync_card(card, bconf): |
100 | + branch_info = get_branch_info([branch]) |
101 | + owner_name = BRANCH_REGEX.match(card.external_system_url).group(1) |
102 | + card_status = get_card_status(None, '', branch_info) |
103 | + card_path = card.lane.path |
104 | + if isinstance(card.lane.board, LeankitTaskBoard): |
105 | + task_card = card.lane.board.parent_card |
106 | + card_path = "{}::{}::{}".format( |
107 | + task_card.lane.path, task_card.title, card_path) |
108 | + if branch_info.status and should_move_card(card, bconf): |
109 | + assignee = Record(name=owner_name) |
110 | + try: |
111 | + move_card(card, card_status, bconf, [assignee], lp_users) |
112 | + except IOError as e: |
113 | + print " * %s (in %s)" % (card.title, card_path) |
114 | + print " >>> Error moving card:" |
115 | + print " ", e |
116 | + continue |
117 | + kanban_user = lp_users.lp_to_kanban.get(assignee.name, None) |
118 | + if kanban_user: |
119 | + card.assigned_user_id = kanban_user.id |
120 | + card.kanban_user = kanban_user |
121 | + else: |
122 | + print " * %s (in %s)" % (card.title, card_path) |
123 | + |
124 | for card in board.getCardsWithExternalIds(): |
125 | if should_sync_card(card, bconf): |
126 | try: |
127 | @@ -475,13 +527,19 @@ |
128 | synced, card_status, assignees, mp_url, mp_type = get_bug_status( |
129 | lp_bug, all_projects, lp_users.lp_to_kanban.keys()) |
130 | |
131 | + card_path = card.lane.path |
132 | + if isinstance(card.lane.board, LeankitTaskBoard): |
133 | + task_card = card.lane.board.parent_card |
134 | + card_path = "{}::{}::{}".format( |
135 | + task_card.lane.path, task_card.title, card_path) |
136 | + |
137 | # Move card to new lane if configured to do so. |
138 | if card_status is not None and should_move_card(card, bconf): |
139 | try: |
140 | move_card(card, card_status, bconf, assignees, lp_users) |
141 | except IOError as e: |
142 | print " * %s: %s (in %s)" % ( |
143 | - bug_id, card.title, card.lane.path) |
144 | + bug_id, card.title, card_path) |
145 | print " >>> Error moving card:" |
146 | print " ", e |
147 | continue |
148 | @@ -500,8 +558,7 @@ |
149 | if mp_url and mp_type: |
150 | card.external_system_url = mp_url |
151 | card.external_system_name = mp_type |
152 | - print " * %s: %s (in %s)" % ( |
153 | - bug_id, card.title, card.lane.path) |
154 | + print " * %s: %s (in %s)" % (bug_id, card.title, card_path) |
155 | else: |
156 | print " * %s: %s -- Not synced" % ( |
157 | bug_id, card.title) |
158 | |
159 | === modified file 'src/lp2kanban/kanban.py' |
160 | --- src/lp2kanban/kanban.py 2014-03-18 20:49:48 +0000 |
161 | +++ src/lp2kanban/kanban.py 2015-12-17 00:04:21 +0000 |
162 | @@ -11,6 +11,8 @@ |
163 | |
164 | |
165 | ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL) |
166 | +BRANCH_REGEX = re.compile( |
167 | + '^https://code.launchpad.net/~([.\w]*)/([-\w]*)/([-\w]*)$') |
168 | |
169 | |
170 | class Record(dict): |
171 | @@ -55,8 +57,7 @@ |
172 | |
173 | class LeankitConnector(object): |
174 | def __init__(self, account, username=None, password=None, throttle=1): |
175 | - host = 'https://' + account + '.leankitkanban.com' |
176 | - self.base_api_url = host + '/Kanban/Api' |
177 | + self.base_api_url = 'https://' + account + '.leankit.com' |
178 | self.http = self._configure_auth(username, password) |
179 | self.last_request_time = time.time() - throttle |
180 | self.throttle = throttle |
181 | @@ -197,14 +198,17 @@ |
182 | optional_attributes = [ |
183 | 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked', |
184 | 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl', |
185 | - 'ClassOfServiceId', 'DueDate', |
186 | + 'ClassOfServiceId', 'DueDate', 'CurrentTaskBoardId' |
187 | ] |
188 | |
189 | def __init__(self, card_dict, lane): |
190 | super(LeankitCard, self).__init__(card_dict) |
191 | |
192 | self.lane = lane |
193 | - self.tags_list = set([tag.strip() for tag in self.tags.split(',')]) |
194 | + if not self.tags: |
195 | + self.tags_list = [] |
196 | + else: |
197 | + self.tags_list = set([tag.strip() for tag in self.tags.split(',')]) |
198 | if '' in self.tags_list: |
199 | self.tags_list.remove('') |
200 | self.type = lane.board.cardtypes[self.type_id] |
201 | @@ -218,7 +222,7 @@ |
202 | tag = tag.strip() |
203 | if tag not in self.tags_list or self.tags.startswith(','): |
204 | if tag != '': |
205 | - self.tags_list.add(tag) |
206 | + self.tags_list.append(tag) |
207 | self.tags = ', '.join(self.tags_list) |
208 | |
209 | def save(self): |
210 | @@ -241,17 +245,20 @@ |
211 | #print "Storing %s in %s..." % (attr, self._toCamelCase(attr)) |
212 | data[self._toCamelCase(attr)] = getattr(self, attr) |
213 | |
214 | + board_id = str(self.lane.board.id) |
215 | + if isinstance(LeankitTaskBoard): |
216 | + board_id = str(self.lane.board.parent_board.id) |
217 | if self.is_new: |
218 | del data['Id'] |
219 | del data['LaneId'] |
220 | position = len(self.lane.cards) |
221 | - url_parts = ['/Board', str(self.lane.board.id), 'AddCard', |
222 | - 'Lane', str(self.lane.id), 'Position', str(position)] |
223 | + url_parts = ['/Kanban/Api/Board', board_id, |
224 | + 'AddCard', 'Lane', str(self.lane.id), |
225 | + 'Position', str(position)] |
226 | else: |
227 | - url_parts = ['/Board', str(self.lane.board.id), 'UpdateCard'] |
228 | + url_parts = ['/Kanban/Api/Board', board_id, 'UpdateCard'] |
229 | |
230 | url = '/'.join(url_parts) |
231 | - |
232 | result = self.lane.board.connector.post(url, data=data) |
233 | |
234 | if (self.is_new and |
235 | @@ -271,9 +278,24 @@ |
236 | else: |
237 | return None |
238 | |
239 | + def moveToTaskBoard(self, target_card): |
240 | + """Move the current card into a subtask of target_card.""" |
241 | + url = '/Api/Card/MoveCardToTaskboard' |
242 | + result = self.lane.board.connector.post(url, data={ |
243 | + 'boardId': self.lane.board.id, |
244 | + 'destCardId': target_card.id, |
245 | + 'srcCardId': [self.id]}) |
246 | + if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES: |
247 | + return result.ReplyData[0] |
248 | + else: |
249 | + raise Exception( |
250 | + "Moving card %s (%s) to %s failed. " % ( |
251 | + self.title, self.id, self.lane.path) + |
252 | + "Error %s: %s" % (result.ReplyCode, result.ReplyText)) |
253 | + |
254 | def _moveCard(self): |
255 | target_pos = len(self.lane.cards) |
256 | - url = '/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % ( |
257 | + url = '/Kanban/Api/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % ( |
258 | self.lane.board.id, self.id, self.lane.id, target_pos) |
259 | result = self.lane.board.connector.post(url, data=None) |
260 | if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES: |
261 | @@ -327,6 +349,8 @@ |
262 | text_after_json), where json_annotations contains the |
263 | JSON loaded with json.loads(). |
264 | """ |
265 | + if not self.description: |
266 | + self.description = "" |
267 | match = ANNOTATION_REGEX.search(self.description) |
268 | if match: |
269 | start = match.start() |
270 | @@ -450,11 +474,12 @@ |
271 | self.cards.append(card) |
272 | return card |
273 | |
274 | + |
275 | class LeankitBoard(Converter): |
276 | |
277 | attributes = ['Id', 'Title', 'CreationDate', 'IsArchived'] |
278 | |
279 | - base_uri = '/Boards/' |
280 | + base_uri = '/Kanban/Api/Boards/' |
281 | |
282 | def __init__(self, board_dict, connector): |
283 | super(LeankitBoard, self).__init__(board_dict) |
284 | @@ -473,12 +498,20 @@ |
285 | self._cards_with_external_ids = set() |
286 | self._cards_with_description_annotations = set() |
287 | self._cards_with_external_links = set() |
288 | + self._cards_with_branches = set() |
289 | self.default_cardtype = None |
290 | |
291 | def getCardsWithExternalIds(self): |
292 | return self._cards_with_external_ids |
293 | |
294 | - def getCardsWithExternalLinks(self): |
295 | + def getCardsWithExternalLinks(self, only_branches=False): |
296 | + """Return cards with external links |
297 | + |
298 | + @param only_merge_proposals: Only return cards that have merge |
299 | + proposals specified as the card's external link |
300 | + """ |
301 | + if only_branches: |
302 | + return self._cards_with_branches |
303 | return self._cards_with_external_links |
304 | |
305 | def getCardsWithDescriptionAnnotations(self): |
306 | @@ -491,15 +524,23 @@ |
307 | self._populateUsers(self.details['BoardUsers']) |
308 | self._populateCardTypes(self.details['CardTypes']) |
309 | self._archive = self.connector.get( |
310 | - "/Board/" + str(self.id) + "/Archive").ReplyData[0] |
311 | + "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0] |
312 | archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive] |
313 | archive_lanes.extend( |
314 | [lane_dict['Lane'] for |
315 | lane_dict in self._archive[0]['ChildLanes']]) |
316 | self._backlog = self.connector.get( |
317 | - "/Board/" + str(self.id) + "/Backlog").ReplyData[0] |
318 | + "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0] |
319 | self._populateLanes( |
320 | self.details['Lanes'] + archive_lanes + self._backlog) |
321 | + for card in self.cards: |
322 | + if card.current_task_board_id: # We are a task board |
323 | + taskboard_data = self.connector.get( |
324 | + "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id)) |
325 | + card.taskboard = LeankitTaskBoard( |
326 | + taskboard_data["ReplyData"][0], self, card) |
327 | + card.taskboard.fetchDetails() |
328 | + self.cards.extend(card.taskboard.cards) |
329 | self._classifyCards() |
330 | |
331 | def _classifyCards(self): |
332 | @@ -512,12 +553,16 @@ |
333 | self._cards_with_description_annotations.add(card) |
334 | if card.external_system_url: |
335 | self._cards_with_external_links.add(card) |
336 | + if BRANCH_REGEX.match(card.external_system_url): |
337 | + self._cards_with_branches.add(card) |
338 | print " - %s cards with external ids" % len( |
339 | self._cards_with_external_ids) |
340 | print " - %s cards with external links" % len( |
341 | self._cards_with_external_links) |
342 | print " - %s cards with description annotations" % len( |
343 | self._cards_with_description_annotations) |
344 | + print " - %s cards with branches" % len( |
345 | + self._cards_with_branches) |
346 | |
347 | def _populateUsers(self, user_data): |
348 | self.users = {} |
349 | @@ -629,6 +674,23 @@ |
350 | self._printLanes(lane, indent, include_cards) |
351 | |
352 | |
353 | +class LeankitTaskBoard(LeankitBoard): |
354 | + |
355 | + attributes = ['Id', 'Title'] |
356 | + |
357 | + def __init__(self, taskboard_dict, board, parent_card): |
358 | + super(LeankitTaskBoard, self).__init__(taskboard_dict, board.connector) |
359 | + self.base_uri = '/Api/Board/%d/TaskBoard/%s/Get' % (board.id, self.id) |
360 | + self.cardtypes = board.cardtypes |
361 | + self.parent_board = board |
362 | + self.parent_card = parent_card |
363 | + self.is_archived = False |
364 | + |
365 | + def fetchDetails(self): |
366 | + self.details = self.connector.get(self.base_uri).ReplyData[0] |
367 | + self._populateLanes(self.details['Lanes']) |
368 | + |
369 | + |
370 | class LeankitKanban(object): |
371 | |
372 | def __init__(self, account, username=None, password=None): |
373 | @@ -642,7 +704,7 @@ |
374 | |
375 | :param include_archived: if True, include archived boards as well. |
376 | """ |
377 | - boards_data = self.connector.get('/Boards').ReplyData |
378 | + boards_data = self.connector.get('/Kanban/Api/Boards').ReplyData |
379 | boards = [] |
380 | for board_dict in boards_data[0]: |
381 | board = LeankitBoard(board_dict, self.connector) |
382 | @@ -687,13 +749,14 @@ |
383 | if __name__ == '__main__': |
384 | kanban = LeankitKanban('launchpad.leankitkanban.com', |
385 | 'user@email', 'password') |
386 | + |
387 | print "Active boards:" |
388 | boards = kanban.getBoards() |
389 | for board in boards: |
390 | print " * %s (%d)" % (board.title, board.id) |
391 | |
392 | # Get a board by the title. |
393 | - board_name = 'lp2kanban test' |
394 | + board_name = 'Landscape Test Board' |
395 | print "Getting board '%s'..." % board_name |
396 | board = kanban.getBoard(title=board_name) |
397 | board.printLanes() |
398 | |
399 | === modified file 'src/lp2kanban/tests/common.py' |
400 | --- src/lp2kanban/tests/common.py 2013-04-08 12:48:10 +0000 |
401 | +++ src/lp2kanban/tests/common.py 2015-12-17 00:04:21 +0000 |
402 | @@ -33,13 +33,16 @@ |
403 | self.is_archived = is_archived |
404 | self._cards_with_description_annotations = set() |
405 | self._cards_with_external_links = set() |
406 | + self._cards_with_branches = set() |
407 | self.lanes = {} |
408 | self.root_lane = self.addLane('ROOT LANE') |
409 | |
410 | def getCardsWithDescriptionAnnotations(self): |
411 | return self._cards_with_description_annotations |
412 | |
413 | - def getCardsWithExternalLinks(self): |
414 | + def getCardsWithExternalLinks(self, only_branches=False): |
415 | + if only_branches: |
416 | + return self._cards_with_branches |
417 | return self._cards_with_external_links |
418 | |
419 | def getLaneByPath(self, path): |
420 | @@ -93,7 +96,7 @@ |
421 | def __init__(self, external_card_id=None, title=u"", description=u"", |
422 | description_annotations=None, lane=None, |
423 | assigned_user_id=None, external_system_name=None, |
424 | - external_system_url=None): |
425 | + external_system_url=u""): |
426 | self.external_card_id = external_card_id |
427 | self.title = title |
428 | self.description = description |
429 | |
430 | === modified file 'src/lp2kanban/tests/test_bugs2cards.py' |
431 | --- src/lp2kanban/tests/test_bugs2cards.py 2013-04-08 12:53:06 +0000 |
432 | +++ src/lp2kanban/tests/test_bugs2cards.py 2015-12-17 00:04:21 +0000 |
433 | @@ -346,6 +346,7 @@ |
434 | # in CODING. |
435 | self.assertEqual(CardStatus.CODING, status) |
436 | |
437 | +BRANCH_URL = "https://code.launchpad.net/~me/project/branch-name" |
438 | |
439 | class CardStatusTest(unittest.TestCase): |
440 | |
441 | @@ -375,23 +376,43 @@ |
442 | |
443 | def test_should_sync_card_autosync_no(self): |
444 | # When autosync is 'on', cards with no external card IDs |
445 | - # are not synced. |
446 | + # and no branches are not synced. |
447 | card = Record(title=u'no sync', description=u'', |
448 | - external_card_id=None) |
449 | + external_card_id=None, external_system_url=None) |
450 | self.assertFalse(should_sync_card(card, {'sync_cards': 'on'})) |
451 | |
452 | - def test_should_sync_card_autosync_yes(self): |
453 | + def test_should_sync_card_autosync_no_sync_with_non_branch_urls(self): |
454 | + # When autosync is 'on', cards with external_system_urls which are not |
455 | + # valid launchpad branches are not synced. |
456 | + non_branch_urls = [ |
457 | + "http://www.google.com/", |
458 | + "https://bugs.launchpad.net/charms/+source/hacluster/+bug/1", |
459 | + "https://code.launchpad.net/~person/project/blah/+merge/111" |
460 | + ] |
461 | + for url in non_branch_urls: |
462 | + card = Record(title=u'no sync', description=u'', |
463 | + external_card_id=None, external_system_url=url) |
464 | + self.assertFalse(should_sync_card(card, {'sync_cards': 'on'})) |
465 | + |
466 | + def test_should_sync_card_autosync_synced_with_external_card_id(self): |
467 | # When autosync is 'on', cards with external card IDs are synced. |
468 | card = Record(title=u'no sync', description=u'', |
469 | - external_card_id='11') |
470 | + external_card_id=u'11', external_system_url=u'') |
471 | + self.assertTrue(should_sync_card(card, {'autosync': 'on', |
472 | + 'sync_cards': 'on'})) |
473 | + |
474 | + def test_should_sync_card_autosync_synced_with_branch(self): |
475 | + # When autosync is 'on', cards with a valid branch are synced. |
476 | + card = Record(title=u'no sync', description=u'', |
477 | + external_card_id=u'11', external_system_url=BRANCH_URL) |
478 | self.assertTrue(should_sync_card(card, {'autosync': 'on', |
479 | 'sync_cards': 'on'})) |
480 | |
481 | def test_should_sync_card_autosync_nosync(self): |
482 | - # When autosync is 'on', cards with external card IDs are not |
483 | - # synced when they contain the no-sync marker. |
484 | + # When autosync is 'on', cards with external card IDs or a valid |
485 | + # branch are not synced when they contain the no-sync marker. |
486 | card = Record(title=u'(no-sync)', description=u'(no-sync)', |
487 | - external_card_id='11') |
488 | + external_card_id=u'11', external_system_url=BRANCH_URL) |
489 | self.assertFalse(should_sync_card(card, {'autosync': 'on', |
490 | 'sync_cards': 'on'})) |
491 | |
492 | @@ -452,7 +473,7 @@ |
493 | self.assertFalse(should_sync_card(card1, conf)) |
494 | self.assertFalse(should_sync_card(card2, conf)) |
495 | |
496 | - def test_get_card_status_noop(self): |
497 | + def test_get_card_status_noop_no_branch(self): |
498 | # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses, |
499 | # the status is unknown. |
500 | self.assertEqual(None, get_card_status('New', [], None)) |
501 | @@ -460,6 +481,33 @@ |
502 | self.assertEqual(None, get_card_status('Incomplete', [], None)) |
503 | self.assertEqual(None, get_card_status('Triaged', [], None)) |
504 | |
505 | + def test_get_card_status_coding_no_bug(self): |
506 | + # For a card with no associated bug, an 'In Progress' branch status |
507 | + # will be in the coding state. |
508 | + branch_info = Record(status='In Progress', target=None) |
509 | + self.assertEqual( |
510 | + CardStatus.CODING, |
511 | + get_card_status(None, [], branch_info)) |
512 | + |
513 | + def test_get_card_status_review_no_bug(self): |
514 | + # For a card with no associated bug, an 'Approved' or 'In Review' |
515 | + # branch status will be in the review state. |
516 | + branch_infos = [ |
517 | + Record(status='In Review', target=None), |
518 | + Record(status='Approved', target=None)] |
519 | + for branch_info in branch_infos: |
520 | + self.assertEqual( |
521 | + CardStatus.REVIEW, |
522 | + get_card_status(None, [], branch_info)) |
523 | + |
524 | + def test_get_card_status_landing_no_bug(self): |
525 | + # For a card with no associated bug, a merged branch status will be in |
526 | + # the landing state. |
527 | + branch_info = Record(status='Merged', target=None) |
528 | + self.assertEqual( |
529 | + CardStatus.LANDING, |
530 | + get_card_status(None, [], branch_info)) |
531 | + |
532 | def test_get_card_status_coding_no_branch(self): |
533 | # For a bug in 'In Progress' or 'Fix Committed' status, it is |
534 | # considered to be in the coding state if there is no branch. |
535 | @@ -496,16 +544,12 @@ |
536 | get_card_status('Fix Committed', [], branch_info)) |
537 | |
538 | def test_get_card_status_landing(self): |
539 | - # For a bug in 'In Progress' or 'Fix Committed' status, it is |
540 | - # considered to be in the landing phase if there is a branch |
541 | - # approved for landing. |
542 | - branch_info = Record(status='Approved', target=None) |
543 | + # For a bug in 'In Progress' status, it is considered to be in the |
544 | + # landing phase if there is a branch 'Merged'. |
545 | + branch_info = Record(status='Merged', target=None) |
546 | self.assertEqual( |
547 | CardStatus.LANDING, |
548 | get_card_status('In Progress', [], branch_info)) |
549 | - self.assertEqual( |
550 | - CardStatus.LANDING, |
551 | - get_card_status('Fix Committed', [], branch_info)) |
552 | |
553 | def test_get_card_status_qa(self): |
554 | # For a bug in the 'Fix Committed' status, it is considered |
Please merge latest lp:~landscape/lp2kanban/landscape-deploy and drop configs altogether (merge them into lp:~landscape/landscape/lp2kanban-configs instead).
Looks good, only minor comments inline.