Merge lp:~dpb/lp2kanban/filter-bugs-correct-movement into lp:lp2kanban

Proposed by David Britton
Status: Merged
Merged at revision: 142
Proposed branch: lp:~dpb/lp2kanban/filter-bugs-correct-movement
Merge into: lp:lp2kanban
Diff against target: 604 lines (+247/-52)
8 files modified
Makefile (+4/-2)
README (+6/-0)
jenkins.sh (+3/-2)
setup.py (+1/-0)
src/lp2kanban/bugs2cards.py (+91/-19)
src/lp2kanban/kanban.py (+31/-23)
src/lp2kanban/tests/common.py (+6/-2)
src/lp2kanban/tests/test_bugs2cards.py (+105/-4)
To merge this branch: bzr merge lp:~dpb/lp2kanban/filter-bugs-correct-movement
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Needs Fixing
Eric Snow (community) Approve
Landscape Pending
Review via email: mp+304273@code.launchpad.net

Commit message

- Remove archive from default board fetch, this speeds up operations significantly. I don't have any way exposed externally from the config to turn this back on, but we don't use this, so I'm skipping for now.
- Remove cards inside taskboard from board fetch. This speeds up operations significantly, and we currently have no use case for automation moving inside a taskboard, nor does the config support doing these kinds of operations for us.
- Add clean make target
- Update README to talk about using a virtualenv for testing
- Add bug filter and card filter for testing individual moves on the board
- Add logging (follow on branch will convert all prints to logging calls
- log interesting paths for easier post-mortem log analysis (Moves and syncs)
- Explicitly skip attempting to move for no-op cases (before we relied on lkk to noop our request for us)

Description of the change

- Remove archive from default board fetch, this speeds up operations significantly. I don't have any way exposed externally from the config to turn this back on, but we don't use this, so I'm skipping for now.
- Remove cards inside taskboard from board fetch. This speeds up operations significantly, and we currently have no use case for automation moving inside a taskboard, nor does the config support doing these kinds of operations for us.
- Add clean make target
- Update README to talk about using a virtualenv for testing
- Add bug filter and card filter for testing individual moves on the board
- Add logging (follow on branch will convert all prints to logging calls
- log interesting paths for easier post-mortem log analysis (Moves and syncs)
- Explicitly skip attempting to move for no-op cases (before we relied on lkk to noop our request for us)

After doing this, I cleaned up lp:~landscape/landscape/lp2kanban-configs, and added better descriptions to that file.

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 :
review: Needs Fixing (test results)
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: 163
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/9632/

review: Approve (test results)
Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
Eric Snow (ericsnowcurrently) :
Revision history for this message
David Britton (dpb) wrote :

Fixed Adam's comments

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: Fail
Revno: 164
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10135/

review: Needs Fixing (test results)
Revision history for this message
David Britton (dpb) wrote :

Addressed all comments.

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: Fail
Revno: 165
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10136/

review: Needs Fixing (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) wrote :

Other than one small fix, +1. Thanks for taking the time on this!

review: Approve
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: Fail
Revno: 166
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10137/

review: Needs Fixing (test results)
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: Fail
Revno: 167
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10138/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

The attempt to merge lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement into lp:lp2kanban failed. Below is the output from the failed tests.

test -d venv || virtualenv venv; \
 . venv/bin/activate; \
 make check
New python executable in venv/bin/python
Installing setuptools, pip...done.
make[1]: Entering directory `/var/tmp/tarmac/lp2kanban/trunk'
mkdir download-cache
python bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg
make[1]: Leaving directory `/var/tmp/tarmac/lp2kanban/trunk'

Traceback (most recent call last):
  File "bootstrap.py", line 80, in <module>
    exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
  File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 404, in open
    response = self._open(req, data)
  File "/usr/lib/python2.7/urllib2.py", line 422, in _open
    '_open', req)
  File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 1222, in https_open
    return self.do_open(httplib.HTTPSConnection, req)
  File "/usr/lib/python2.7/urllib2.py", line 1184, in do_open
    raise URLError(err)
urllib2.URLError: <urlopen error Tunnel connection failed: 403 Forbidden>
make[1]: *** [bin/buildout] Error 1
make: *** [ci-test] Error 2

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

No approved revision specified.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2016-08-29 20:18:23 +0000
+++ Makefile 2016-09-01 21:37:25 +0000
@@ -17,12 +17,14 @@
17# buildout.17# buildout.
18BUILDOUT_BIN = $(PY) bin/tags bin/test18BUILDOUT_BIN = $(PY) bin/tags bin/test
1919
20default: check20default: bin/test
2121
2222
23download-cache:23download-cache:
24 mkdir download-cache24 mkdir download-cache
2525
26clean:
27 @rm -rf bin
2628
27bin/buildout: download-cache29bin/buildout: download-cache
28 $(PYTHON) bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg30 $(PYTHON) bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg
@@ -59,4 +61,4 @@
59 . venv/bin/activate; \61 . venv/bin/activate; \
60 $(MAKE) check62 $(MAKE) check
6163
62.PHONY: check default configs needs-xdg-utils ci-test64.PHONY: check default configs needs-xdg-utils clean ci-test
6365
=== modified file 'README'
--- README 2012-12-10 15:21:44 +0000
+++ README 2016-09-01 21:37:25 +0000
@@ -38,5 +38,11 @@
38Testing38Testing
39-------39-------
4040
41All tests should work on 16.04+, please run in a vitrualenv though:
42
43 virtualenv ve
44 source ve/bin/activate
45 make check
46
41'lp2kanban test' board configuration connects to LP staging by default47'lp2kanban test' board configuration connects to LP staging by default
42to allow easier testing.48to allow easier testing.
4349
=== modified file 'jenkins.sh'
--- jenkins.sh 2015-12-16 16:56:38 +0000
+++ jenkins.sh 2016-09-01 21:37:25 +0000
@@ -1,6 +1,5 @@
1#!/bin/bash -xe1#!/bin/bash -xe
22
3ini="$1.ini"
4date "+START: %c"3date "+START: %c"
54
6#export https_proxy=http://squid.external:31285#export https_proxy=http://squid.external:3128
@@ -28,5 +27,7 @@
28# - https://stackoverflow.com/questions/147357727# - https://stackoverflow.com/questions/1473577
29# - https://stackoverflow.com/questions/49248328# - https://stackoverflow.com/questions/492483
30# - https://stackoverflow.com/questions/993240629# - https://stackoverflow.com/questions/9932406
31PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini30config=$1
31shift
32PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c "configs/${config}.ini" "${@}"
32date "+END: %c"33date "+END: %c"
3334
=== modified file 'setup.py'
--- setup.py 2016-04-13 16:06:23 +0000
+++ setup.py 2016-09-01 21:37:25 +0000
@@ -18,6 +18,7 @@
18 'requests',18 'requests',
19 'setuptools',19 'setuptools',
20 'simplejson',20 'simplejson',
21 'testfixtures',
21 'testtools',22 'testtools',
22 ],23 ],
23 )24 )
2425
=== modified file 'src/lp2kanban/bugs2cards.py'
--- src/lp2kanban/bugs2cards.py 2016-08-26 17:11:41 +0000
+++ src/lp2kanban/bugs2cards.py 2016-09-01 21:37:25 +0000
@@ -3,6 +3,7 @@
3from ConfigParser import ConfigParser3from ConfigParser import ConfigParser
4from argparse import ArgumentParser4from argparse import ArgumentParser
5from launchpadlib.launchpad import Launchpad5from launchpadlib.launchpad import Launchpad
6import logging
6from lp2kanban.cards2workitems import (7from lp2kanban.cards2workitems import (
7 convert_cards_to_work_items,8 convert_cards_to_work_items,
8 get_blueprint_linked_cards,9 get_blueprint_linked_cards,
@@ -45,7 +46,13 @@
45 help="List available kanban boards.")46 help="List available kanban boards.")
46 parser.add_argument(47 parser.add_argument(
47 '-d', '--debug', action='store_const', const=True, default=False,48 '-d', '--debug', action='store_const', const=True, default=False,
48 help="Turn on debugging for HTTP traffic")49 help="Turn on logging debug, debugging for HTTP traffic")
50 parser.add_argument(
51 '-B', '--bug', required=False, type=int, default=None,
52 help="Only operate on this bug.")
53 parser.add_argument(
54 '-C', '--card', required=False, type=int, default=None,
55 help="Only operate on this card id, can obtain by looking at URL.")
49 return parser56 return parser
5057
5158
@@ -207,7 +214,8 @@
207 return lp_bug214 return lp_bug
208215
209216
210def move_cards_to_feature_taskboard(feature_card, feature_bug, source_lanes):217def move_cards_to_feature_taskboard(
218 feature_card, feature_bug, source_lanes, filter_card=None):
211 """Move all cards related to subtasks of the feature_card.219 """Move all cards related to subtasks of the feature_card.
212220
213 This will only move cards which are in the landing_lanes or deploy_lanes.221 This will only move cards which are in the landing_lanes or deploy_lanes.
@@ -220,6 +228,8 @@
220 """228 """
221 linked_cards = get_cards_for_feature(feature_card, feature_bug=feature_bug)229 linked_cards = get_cards_for_feature(feature_card, feature_bug=feature_bug)
222 for card in linked_cards:230 for card in linked_cards:
231 if filter_card and filter_card != card.id:
232 return
223 if card.type.name == "Feature": # We don't want to move feature cards233 if card.type.name == "Feature": # We don't want to move feature cards
224 continue234 continue
225 card_path = card.lane.path235 card_path = card.lane.path
@@ -349,11 +359,14 @@
349 """359 """
350 if conf.get('sync_cards', 'off') == 'off':360 if conf.get('sync_cards', 'off') == 'off':
351 return False361 return False
362 logging.debug("Should I sync? - {}".format(card.title))
352 no_sync_lanes = [363 no_sync_lanes = [
353 lane_path.strip()364 lane_path.strip()
354 for lane_path in conf.get("no_sync_lanes", "").split(",")]365 for lane_path in conf.get("no_sync_lanes", "").split(",")]
355 for no_sync_lane_path in no_sync_lanes:366 for no_sync_lane_path in no_sync_lanes:
356 if no_sync_lane_path and card.lane.path.endswith(no_sync_lane_path):367 if no_sync_lane_path and card.lane.path.endswith(no_sync_lane_path):
368 logging.debug("Not syncing, {} in no_sync_lanes".format(
369 card.lane.path))
357 return False370 return False
358 if conf.get('autosync', None) in ['on', 'active-projects']:371 if conf.get('autosync', None) in ['on', 'active-projects']:
359 has_id = (card.external_card_id is not None and372 has_id = (card.external_card_id is not None and
@@ -666,6 +679,7 @@
666679
667def create_cards(board, bconf, lp_users, lp_projects):680def create_cards(board, bconf, lp_users, lp_projects):
668 """Create new cards on all projects."""681 """Create new cards on all projects."""
682 logging.info("Creating new cards")
669 new_bug_tag = bconf.get("bug_to_card_tag")683 new_bug_tag = bconf.get("bug_to_card_tag")
670 if new_bug_tag is not None:684 if new_bug_tag is not None:
671 for lp_project in lp_projects:685 for lp_project in lp_projects:
@@ -674,10 +688,13 @@
674 bconf.get("bug_to_card_lane"), lp_users.feature_lanes)688 bconf.get("bug_to_card_lane"), lp_users.feature_lanes)
675689
676690
677def sync_cards_linked_branches(board, bconf, lp, lp_users):691def sync_cards_linked_branches(
692 board, bconf, lp, lp_users, filter_card=None):
678 """Sync any cards that have linked external branches."""693 """Sync any cards that have linked external branches."""
679 print " Syncing branch cards:"694 logging.info("Syncing branch cards")
680 for card in board.getCardsWithExternalLinks(only_branches=True):695 for card in board.getCardsWithExternalLinks(only_branches=True):
696 if filter_card and filter_card != card.id:
697 continue
681 if card.external_card_id:698 if card.external_card_id:
682 # Cards with external_id are processed by getCardsWithExternalIds699 # Cards with external_id are processed by getCardsWithExternalIds
683 continue700 continue
@@ -714,9 +731,16 @@
714 print " * %s (in %s)" % (card.title, card_path)731 print " * %s (in %s)" % (card.title, card_path)
715732
716733
717def sync_cards_external_ids(board, bconf, lp, lp_users, lp_projects):734def sync_cards_external_ids(
735 board, bconf, lp, lp_users, lp_projects, filter_bug=None,
736 filter_card=None):
718 """Sync any cards that have linked external ids (bugs in launchpad)."""737 """Sync any cards that have linked external ids (bugs in launchpad)."""
738 logging.info("Syncing cards with external ids")
719 for card in board.getCardsWithExternalIds():739 for card in board.getCardsWithExternalIds():
740 if filter_bug and card.external_card_id != filter_bug:
741 continue
742 if filter_card and filter_card != card.id:
743 continue
720 if should_sync_card(card, bconf):744 if should_sync_card(card, bconf):
721 lp_bug = get_lp_bug(card, lp)745 lp_bug = get_lp_bug(card, lp)
722 if lp_bug is None:746 if lp_bug is None:
@@ -763,8 +787,10 @@
763 print " * %s: %s -- Not synced" % (787 print " * %s: %s -- Not synced" % (
764 lp_bug.id, card.title)788 lp_bug.id, card.title)
765789
766def move_cards_to_taskboards(board, bconf, lp, lp_users):790def move_cards_to_taskboards(
791 board, bconf, lp, lp_users, filter_card=None):
767 """Move branch cards to feature cards if they are landed."""792 """Move branch cards to feature cards if they are landed."""
793 logging.info("Move cards to taskboards")
768 if "${group}" in bconf["landing_lanes"]:794 if "${group}" in bconf["landing_lanes"]:
769 # Replace the optional template ${group} with configured board groups.795 # Replace the optional template ${group} with configured board groups.
770 # This creates a list of landing_lanes, one per group on the board.796 # This creates a list of landing_lanes, one per group on the board.
@@ -779,11 +805,12 @@
779 feature_card.lane.path in valid_taskboard_lanes):805 feature_card.lane.path in valid_taskboard_lanes):
780 lp_bug = get_lp_bug(feature_card, lp)806 lp_bug = get_lp_bug(feature_card, lp)
781 move_cards_to_feature_taskboard(807 move_cards_to_feature_taskboard(
782 feature_card, lp_bug, valid_taskboard_lanes)808 feature_card, lp_bug, valid_taskboard_lanes, filter_card)
783809
784810
785def convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects):811def convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects):
786 """Create work items out of cards if desired."""812 """Create work items out of cards if desired."""
813 logging.info("Convert cards to work items")
787 if bconf.get("convert_cards_to_work_items", False) == "on":814 if bconf.get("convert_cards_to_work_items", False) == "on":
788 # Sync cards to blueprint work items, if so configured.815 # Sync cards to blueprint work items, if so configured.
789 print " Checking for cards to sync with work items..."816 print " Checking for cards to sync with work items..."
@@ -808,25 +835,46 @@
808835
809 If a card is located in a "no_move" lane, the card won't get moved.836 If a card is located in a "no_move" lane, the card won't get moved.
810 """837 """
838 logging.debug("Evaluating for move: {}".format(card.title))
811 target_lane_path = bconf[ConfigOptionByStatus[bug_status]]839 target_lane_path = bconf[ConfigOptionByStatus[bug_status]]
812 if assignees:840 if "${group}" in target_lane_path:
813 group = lp_users.lp_to_group.get(assignees[0].name)841 if assignees:
814 if group:842 group = lp_users.lp_to_group.get(assignees[0].name)
815 target_lane_path = target_lane_path.replace("${group}", group)843 if group:
844 target_lane_path = target_lane_path.replace("${group}", group)
845 else:
846 logging.debug(
847 "Not moving, assignee: {} not in group: {}".format(
848 assignees[0].name, group))
849 return
850 else:
851 logging.debug("Not moving, not assigned")
852 return
853
816 omnidirectional_card_moves = (854 omnidirectional_card_moves = (
817 bconf.get("omnidirectional_card_moves", "on") == "on")855 bconf.get("omnidirectional_card_moves", "on") == "on")
818 no_move_lanes = [856 no_move_lanes = [
819 lane_path.strip()857 lane_path.strip()
820 for lane_path in bconf.get("no_move_lanes", "").split(",")]858 for lane_path in bconf.get("no_move_lanes", "").split(",")
859 if lane_path]
821 for no_move_lane_path in no_move_lanes:860 for no_move_lane_path in no_move_lanes:
822 if no_move_lane_path and card.lane.path.endswith(no_move_lane_path):861 if no_move_lane_path and card.lane.path.endswith(no_move_lane_path):
862 logging.debug("Not moving, {} in no_move_lanes".format(
863 card.lane.path))
823 return864 return
865
824 if omnidirectional_card_moves:866 if omnidirectional_card_moves:
825 target_lane = find_card_target_lane_omnidirectional(867 target_lane = find_card_target_lane_omnidirectional(
826 card.lane, target_lane_path)868 card.lane, target_lane_path)
827 else:869 else:
828 target_lane = find_card_target_lane_next(870 target_lane = find_card_target_lane_next(
829 card.lane, target_lane_path)871 card.lane, target_lane_path)
872 if card.lane.path == target_lane_path:
873 logging.debug("Not moving, already in {}".format(card.lane.path))
874 return
875
876 logging.debug(
877 " * Moving: {} -> {}".format(card.lane.path, target_lane_path))
830 card.move(target_lane)878 card.move(target_lane)
831879
832880
@@ -853,13 +901,43 @@
853 continue901 continue
854902
855903
904def process_board(board, bconf, filter_bug, filter_card):
905 """Sync a given board.
906
907 Decomposition of main, and handles interpreting
908 filter arguments.
909 """
910 lp, lp_projects, lp_users = get_lp_data(board, bconf)
911 if not filter_bug and not filter_card:
912 create_cards(board, bconf, lp_users, lp_projects)
913 if not filter_bug:
914 sync_cards_linked_branches(
915 board, bconf, lp, lp_users, filter_card=filter_card)
916 sync_cards_external_ids(
917 board, bconf, lp, lp_users, lp_projects, filter_bug=filter_bug,
918 filter_card=filter_card)
919 if not filter_bug:
920 move_cards_to_taskboards(
921 board, bconf, lp, lp_users, filter_card=filter_card)
922 if not filter_bug and not filter_card:
923 convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects)
924 save_board(board)
925
926
856def main(argv):927def main(argv):
857 """Update the Yellow team's kanban board with data from Launchpad."""928 """Update the Yellow team's kanban board with data from Launchpad."""
858 args = get_arg_parser().parse_args()929 args = get_arg_parser().parse_args()
930 if args.bug:
931 args.bug = str(args.bug)
859932
860 if args.debug:933 if args.debug:
934 logging.basicConfig(
935 level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
861 import httplib936 import httplib
862 httplib.HTTPConnection.debuglevel = 1937 httplib.HTTPConnection.debuglevel = 1
938 else:
939 logging.basicConfig(
940 level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
863941
864 config = ConfigParser()942 config = ConfigParser()
865 config.read(args.config)943 config.read(args.config)
@@ -891,13 +969,7 @@
891 print "Could not retrieve board. Skipping."969 print "Could not retrieve board. Skipping."
892 continue970 continue
893 bconf = boards_config[board_name]971 bconf = boards_config[board_name]
894 lp, lp_projects, lp_users = get_lp_data(board, bconf)972 process_board(board, bconf, args.bug, args.card)
895 create_cards(board, bconf, lp_users, lp_projects)
896 sync_cards_linked_branches(board, bconf, lp, lp_users)
897 sync_cards_external_ids(board, bconf, lp, lp_users, lp_projects)
898 move_cards_to_taskboards(board, bconf, lp, lp_users)
899 convert_cards_to_work_items(board, bconf, lp, lp_users)
900 save_board(board)
901 except (KeyError, AssertionError) as e:973 except (KeyError, AssertionError) as e:
902 print "Exception caught. Skipping board."974 print "Exception caught. Skipping board."
903 print_exc()975 print_exc()
904976
=== modified file 'src/lp2kanban/kanban.py'
--- src/lp2kanban/kanban.py 2016-04-14 20:50:10 +0000
+++ src/lp2kanban/kanban.py 2016-09-01 21:37:25 +0000
@@ -3,6 +3,7 @@
33
4import requests4import requests
5import json5import json
6import logging
6import operator7import operator
7from pprint import pprint8from pprint import pprint
8import re9import re
@@ -180,9 +181,8 @@
180 return name.upper()181 return name.upper()
181182
182 def __setattr__(self, attr, value):183 def __setattr__(self, attr, value):
183 if ((not hasattr(self, attr) or184 if (getattr(self, attr, object()) != value and
184 getattr(self, attr, None) != value) and185 attr in self._watched_attrs):
185 attr in self._watched_attrs):
186 self.direct_setattr('is_dirty', True)186 self.direct_setattr('is_dirty', True)
187 self.dirty_attrs.add(attr)187 self.dirty_attrs.add(attr)
188 self.direct_setattr(attr, value)188 self.direct_setattr(attr, value)
@@ -531,30 +531,38 @@
531 def getCardsWithDescriptionAnnotations(self):531 def getCardsWithDescriptionAnnotations(self):
532 return self._cards_with_description_annotations532 return self._cards_with_description_annotations
533533
534 def fetchDetails(self):534 def fetchDetails(self, include_archive=False, include_taskboards=False):
535 """Fetch all details from the board into our datastructure.
536
537 include_taskboards - add all cards on taskboards into my card list
538 include_archive - add all cards from archive into my card list.
539 """
535 self.details = self.connector.get(540 self.details = self.connector.get(
536 self.base_uri + str(self.id)).ReplyData[0]541 self.base_uri + str(self.id)).ReplyData[0]
537542
538 self._populateUsers(self.details['BoardUsers'])543 self._populateUsers(self.details['BoardUsers'])
539 self._populateCardTypes(self.details['CardTypes'])544 self._populateCardTypes(self.details['CardTypes'])
540 self._archive = self.connector.get(545 lanes = self.details['Lanes']
541 "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]546 if include_archive:
542 archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]547 self._archive = self.connector.get(
543 archive_lanes.extend(548 "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]
544 [lane_dict['Lane'] for549 archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
545 lane_dict in self._archive[0]['ChildLanes']])550 archive_lanes.extend(
546 self._backlog = self.connector.get(551 [lane_dict['Lane'] for
552 lane_dict in self._archive[0]['ChildLanes']])
553 lanes += archive_lanes
554 lanes += self.connector.get(
547 "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0]555 "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0]
548 self._populateLanes(556 self._populateLanes(lanes)
549 self.details['Lanes'] + archive_lanes + self._backlog)557 if include_taskboards:
550 for card in self.cards:558 for card in self.cards:
551 if card.current_task_board_id: # We are a task board559 if card.current_task_board_id: # We are a task board
552 taskboard_data = self.connector.get(560 taskboard_data = self.connector.get(
553 "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))561 "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
554 card.taskboard = LeankitTaskBoard(562 card.taskboard = LeankitTaskBoard(
555 taskboard_data["ReplyData"][0], self, card)563 taskboard_data["ReplyData"][0], self, card)
556 card.taskboard.fetchDetails()564 card.taskboard.fetchDetails()
557 self.cards.extend(card.taskboard.cards)565 self.cards.extend(card.taskboard.cards)
558 self._classifyCards()566 self._classifyCards()
559 self._populateFeatureTagPaths()567 self._populateFeatureTagPaths()
560568
@@ -769,10 +777,10 @@
769 board = self._findBoardInCache(board_id, title)777 board = self._findBoardInCache(board_id, title)
770 return board778 return board
771779
772 def getBoard(self, board_id=None, title=None):780 def getBoard(self, board_id=None, title=None, include_archive=False):
773 board = self._findBoard(board_id, title)781 board = self._findBoard(board_id, title)
774 if board is not None:782 if board is not None:
775 board.fetchDetails()783 board.fetchDetails(include_archive=include_archive)
776 return board784 return board
777785
778786
779787
=== modified file 'src/lp2kanban/tests/common.py'
--- src/lp2kanban/tests/common.py 2016-05-02 23:07:28 +0000
+++ src/lp2kanban/tests/common.py 2016-09-01 21:37:25 +0000
@@ -1,10 +1,13 @@
1# Copyright 2012 Canonical Ltd.1# Copyright 2012 Canonical Ltd.
2"""Common fixtures for lp2kanban tests."""2"""Common fixtures for lp2kanban tests."""
33
4import uuid
5
6from lp2kanban.kanban import Record
7
8
4__metaclass__ = type9__metaclass__ = type
510
6from lp2kanban.kanban import Record
7
811
9class FauxCardType:12class FauxCardType:
1013
@@ -123,6 +126,7 @@
123 self.external_system_url = external_system_url126 self.external_system_url = external_system_url
124 self.moved_to = None127 self.moved_to = None
125 self.saved = False128 self.saved = False
129 self.id = uuid.uuid1()
126130
127 def save(self):131 def save(self):
128 """A no-op for compatibility."""132 """A no-op for compatibility."""
129133
=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
--- src/lp2kanban/tests/test_bugs2cards.py 2016-08-25 22:37:31 +0000
+++ src/lp2kanban/tests/test_bugs2cards.py 2016-09-01 21:37:25 +0000
@@ -13,6 +13,8 @@
13import datetime13import datetime
14import unittest14import unittest
1515
16from testfixtures import log_capture
17
16from lp2kanban.bugs2cards import (18from lp2kanban.bugs2cards import (
17 CardStatus,19 CardStatus,
18 create_cards_in_project,20 create_cards_in_project,
@@ -50,7 +52,6 @@
50 )52 )
5153
5254
53
54def parse_config_text(text):55def parse_config_text(text):
55 config = ConfigParser()56 config = ConfigParser()
56 config.readfp(StringIO(dedent(text)))57 config.readfp(StringIO(dedent(text)))
@@ -1489,7 +1490,18 @@
1489 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)1490 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)
1490 self.assertEqual("coding", card.moved_to.path)1491 self.assertEqual("coding", card.moved_to.path)
14911492
1492 def test_group_no_assignee(self):1493 @log_capture()
1494 def test_noop(self, testlog):
1495 """Skip move attempt if already in right path."""
1496 card = self.qa.addCard(FauxCard())
1497 move_card(card, CardStatus.QA, self.bconf, [], self.lp_users)
1498 self.assertEqual(None, card.moved_to)
1499 testlog.check(
1500 ('root', 'DEBUG', 'Evaluating for move: '),
1501 ('root', 'DEBUG', 'Not moving, already in qa'))
1502
1503 @log_capture()
1504 def test_group_no_assignee(self, testlog):
1493 # If a target lane requires a group, and the card doesn't have1505 # If a target lane requires a group, and the card doesn't have
1494 # an assignee, the card isn't moved.1506 # an assignee, the card isn't moved.
1495 self.bconf["coding_lanes"] = "${group}::coding"1507 self.bconf["coding_lanes"] = "${group}::coding"
@@ -1497,6 +1509,9 @@
1497 card = self.backlog.addCard(FauxCard())1509 card = self.backlog.addCard(FauxCard())
1498 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)1510 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)
1499 self.assertEqual(None, card.moved_to)1511 self.assertEqual(None, card.moved_to)
1512 testlog.check(
1513 ('root', 'DEBUG', 'Evaluating for move: '),
1514 ('root', 'DEBUG', 'Not moving, not assigned'))
15001515
1501 def test_group_assignee_in_group(self):1516 def test_group_assignee_in_group(self):
1502 # If a target lane requires a group, and the card has an1517 # If a target lane requires a group, and the card has an
@@ -1511,7 +1526,8 @@
1511 self.lp_users)1526 self.lp_users)
1512 self.assertEqual("test group::coding", card.moved_to.path)1527 self.assertEqual("test group::coding", card.moved_to.path)
15131528
1514 def test_group_assignee_not_in_group(self):1529 @log_capture()
1530 def test_group_assignee_not_in_group(self, testlog):
1515 # If a target lane requires a group, and the card has an1531 # If a target lane requires a group, and the card has an
1516 # assignee that is not part of a group, the card isn't moved.1532 # assignee that is not part of a group, the card isn't moved.
1517 self.bconf["coding_lanes"] = "${group}::coding"1533 self.bconf["coding_lanes"] = "${group}::coding"
@@ -1522,6 +1538,11 @@
1522 card, CardStatus.CODING, self.bconf, [FauxPerson("bar")],1538 card, CardStatus.CODING, self.bconf, [FauxPerson("bar")],
1523 self.lp_users)1539 self.lp_users)
1524 self.assertEqual(None, card.moved_to)1540 self.assertEqual(None, card.moved_to)
1541 testlog.check(
1542 ('root', 'DEBUG', 'Evaluating for move: '),
1543 ('root', 'DEBUG', 'Not moving, assignee: bar not in group: None'))
1544
1545
15251546
1526 def test_no_move_lanes(self):1547 def test_no_move_lanes(self):
1527 # Cards in a "no_move" lane doesn't get moved by move_card().1548 # Cards in a "no_move" lane doesn't get moved by move_card().
@@ -1709,6 +1730,86 @@
1709 self.assertEqual(self.feature_card, landed_card1.moved_to)1730 self.assertEqual(self.feature_card, landed_card1.moved_to)
1710 self.assertEqual(self.feature_card, landed_card2.moved_to)1731 self.assertEqual(self.feature_card, landed_card2.moved_to)
17111732
1733 def test_sync_cards_external_ids_filter_card(self):
1734 """filter_card skips and selects correctly."""
1735 # Configure bugs2cards to sync only active-projects myproject1 and myproject2
1736 self.bconf.update({
1737 "autosync": "active-projects",
1738 "projects": "myproject",
1739 "sync_cards": "on", "move_cards": "on",
1740 "new_lanes": "new"})
1741 # Setup bug1 and bug2 in separate projects which both need to move coding -> new
1742 project1 = FauxTarget(resource_type_link="project", project="myproject")
1743 lp_users = FauxLaunchpadUsersForBoard(lp_to_kanban={"person1": "Person One"})
1744 lp_projects = [project1]
1745 bug_task1 = FauxBugTask(
1746 bug_id=123, target=project1, title='Test bug in myproject',
1747 description='Test bug description.')
1748 bug_task2 = FauxBugTask(
1749 bug_id=456, target=project1, title='Test bug in otherproject',
1750 description='Test bug description.')
1751 bug_card1 = FauxCard(
1752 title='BugCard123', external_card_id=u'123')
1753 bug_card2 = FauxCard(
1754 title='BugCard456', external_card_id=u'456')
1755 board = FauxBoard(
1756 cards=[bug_card1, bug_card2], bug_cards=[bug_card1, bug_card2])
1757 new_lane = board.addLane('new')
1758 coding_lane = board.addLane('coding')
1759 bug_card1.lane = coding_lane
1760 bug_card2.lane = coding_lane
1761 bug_type = FauxCardType(id=3, name='Bug', is_default=False)
1762 bug_card1.type = bug_type
1763 bug_card2.type = bug_type
1764 self.board.cards.extend([bug_card1, bug_card2])
1765 lp = FauxLP({"projects": {"myproject": project1},
1766 "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
1767 sync_cards_external_ids(
1768 board, self.bconf, lp, lp_users, lp_projects, filter_card=bug_card2.id)
1769 coding_lane = FauxLane(board=board, path='coding')
1770 self.assertEqual(new_lane, bug_card2.moved_to)
1771 self.assertIsNone(bug_card1.moved_to)
1772
1773 def test_sync_cards_skips_bugs_not_in_filter_bug(self):
1774 """filter_bug skips and selects bugs in sync_cards_external_ids."""
1775 # Configure bugs2cards to sync only active-projects myproject1 and myproject2
1776 self.bconf.update({
1777 "autosync": "active-projects",
1778 "projects": "myproject",
1779 "sync_cards": "on", "move_cards": "on",
1780 "new_lanes": "new"})
1781 # Setup bug1 and bug2 in separate projects which both need to move coding -> new
1782 project1 = FauxTarget(resource_type_link="project", project="myproject")
1783 lp_users = FauxLaunchpadUsersForBoard(lp_to_kanban={"person1": "Person One"})
1784 lp_projects = [project1]
1785 bug_task1 = FauxBugTask(
1786 bug_id=123, target=project1, title='Test bug in myproject',
1787 description='Test bug description.')
1788 bug_task2 = FauxBugTask(
1789 bug_id=456, target=project1, title='Test bug in otherproject',
1790 description='Test bug description.')
1791 bug_card1 = FauxCard(
1792 title='BugCard123', external_card_id=u'123')
1793 bug_card2 = FauxCard(
1794 title='BugCard456', external_card_id=u'456')
1795 board = FauxBoard(
1796 cards=[bug_card1, bug_card2], bug_cards=[bug_card1, bug_card2])
1797 new_lane = board.addLane('new')
1798 coding_lane = board.addLane('coding')
1799 bug_card1.lane = coding_lane
1800 bug_card2.lane = coding_lane
1801 bug_type = FauxCardType(id=3, name='Bug', is_default=False)
1802 bug_card1.type = bug_type
1803 bug_card2.type = bug_type
1804 self.board.cards.extend([bug_card1, bug_card2])
1805 lp = FauxLP({"projects": {"myproject": project1},
1806 "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
1807 sync_cards_external_ids(
1808 board, self.bconf, lp, lp_users, lp_projects, filter_bug=u"456")
1809 coding_lane = FauxLane(board=board, path='coding')
1810 self.assertEqual(new_lane, bug_card2.moved_to)
1811 self.assertIsNone(bug_card1.moved_to)
1812
1712 def test_sync_cards_skips_bugs_of_external_projects_when_active_projects_set(self):1813 def test_sync_cards_skips_bugs_of_external_projects_when_active_projects_set(self):
1713 """Bugs of external projects are skipped when autosync set to active-projects."""1814 """Bugs of external projects are skipped when autosync set to active-projects."""
1714 # Configure bugs2cards to sync only active-projects myproject1 and myproject21815 # Configure bugs2cards to sync only active-projects myproject1 and myproject2
@@ -1744,7 +1845,7 @@
1744 self.board.cards.extend([bug_card1, bug_card2])1845 self.board.cards.extend([bug_card1, bug_card2])
1745 # Include project1 and project2 in our active 'projects' fake LP data1846 # Include project1 and project2 in our active 'projects' fake LP data
1746 lp = FauxLP({"projects": {"myproject": project1, "myproject2": project2},1847 lp = FauxLP({"projects": {"myproject": project1, "myproject2": project2},
1747 "bugs": {123: bug_task1.bug, "456": bug_task2.bug}})1848 "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
1748 sync_cards_external_ids(board, self.bconf, lp, lp_users, lp_projects)1849 sync_cards_external_ids(board, self.bconf, lp, lp_users, lp_projects)
1749 coding_lane = FauxLane(board=board, path='coding')1850 coding_lane = FauxLane(board=board, path='coding')
1750 self.assertEqual(new_lane, bug_card1.moved_to)1851 self.assertEqual(new_lane, bug_card1.moved_to)

Subscribers

People subscribed via source and target branches

to all changes: