Merge lp:~chad.smith/lp2kanban/branch-to-card into lp:~launchpad/lp2kanban/trunk

Proposed by Chad Smith
Status: Superseded
Proposed branch: lp:~chad.smith/lp2kanban/branch-to-card
Merge into: lp:~launchpad/lp2kanban/trunk
Diff against target: 964 lines (+460/-83)
14 files modified
.bzrignore (+1/-0)
Makefile (+2/-2)
README.landscape (+17/-0)
bootstrap.py (+66/-42)
buildout.cfg (+2/-2)
configs/groups.ini (+16/-0)
configs/sync.ini (+106/-0)
create_creds.py (+1/-1)
jenkins.sh (+32/-0)
run.sh (+21/-0)
src/lp2kanban/bugs2cards.py (+57/-10)
src/lp2kanban/kanban.py (+78/-16)
src/lp2kanban/tests/common.py (+5/-2)
src/lp2kanban/tests/test_bugs2cards.py (+56/-8)
To merge this branch: bzr merge lp:~chad.smith/lp2kanban/branch-to-card
Reviewer Review Type Date Requested Status
Данило Шеган (community) Needs Resubmitting
Landscape Pending
Review via email: mp+280669@code.launchpad.net

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/kanban.py:
- Define initial LeankitTaskBoard class
- Add optional CurrentTaskBoardId property to LeankitCard
- Add LeankitCard.moveToTaskBoard method to move a card as a subtask of another card
- Since Taskboard API calls live on a different route, rework API routes defined on the LeankitConnector class

src/lp2kanban/bugs2cards.py:
- _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_system_urls linked but no bugs (external_system_id) linked
- add a loop to sync_board function which processes all "branch cards" through board.getCardsWithExternalLinks(only_branches=True)

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 landscape 201
./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

./bin/py src/lp2kanban/bugs2cards.py -c configs/sync.ini -b 'Landscape 2016'

# Tweak branches or drag cards at https://canonical.leankit.com/Boards/View/102392996#workflow-view that you want to see automatically moved back to the proper lanes.

What's lacking:
 unit tests to cover bugs2card sync_board (coming in a followup branch)

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :

Please re-submit this against lp:~landscape/lp2kanban/landscape-deploy.

review: Needs Resubmitting
Revision history for this message
Данило Шеган (danilo) wrote :

Note that I've also split out configs now since it was too easy to leak credentials (a bzr push in the lp2kanban namespace creates a public branch). Please review my drop-configs branch.

The kanban-sync job has been updated to use the new credentials (I've updated the password as well).

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

Unmerged revisions

142. By Chad Smith

revert test makefile change

141. By Chad Smith

merge landscape-deploy to get latest changes to jenkins.sh

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

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

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

137. By Chad Smith

address review comments: approved branches that have not yet been merged will remain in the status LANDING. update copyright.

136. By Chad Smith

merge ~landscape/lp2kanban/landscape-deploy to drop configs

135. By Chad Smith

simplify moveToTaskBoard card method

134. By Chad Smith

process cards with linked branches in bugs2cards since a lot of the logic is common

133. By Chad Smith

landing_lanes == Dev::::Landed for config

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2012-09-15 18:20:28 +0000
+++ .bzrignore 2015-12-16 04:22:55 +0000
@@ -14,3 +14,4 @@
14.emacs.desktop14.emacs.desktop
15credentials15credentials
16.emacs.desktop.lock16.emacs.desktop.lock
17logs/*
1718
=== modified file 'Makefile'
--- Makefile 2013-03-18 15:45:56 +0000
+++ Makefile 2015-12-16 04:22:55 +0000
@@ -23,12 +23,12 @@
2323
2424
25bin/buildout: download-cache25bin/buildout: download-cache
26 $(PYTHON) bootstrap.py -v 1.7.026 $(PYTHON) bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg
27 touch --no-create $@27 touch --no-create $@
2828
2929
30$(PY): bin/buildout $(BUILDOUT_CFG) setup.py30$(PY): bin/buildout $(BUILDOUT_CFG) setup.py
31 PYTHONPATH=. ./bin/buildout -c $(BUILDOUT_CFG)31 ./bin/buildout -c $(BUILDOUT_CFG)
3232
3333
34$(subst $(PY),,$(BUILDOUT_BIN)): $(PY)34$(subst $(PY),,$(BUILDOUT_BIN)): $(PY)
3535
=== added file 'README.landscape'
--- README.landscape 1970-01-01 00:00:00 +0000
+++ README.landscape 2015-12-16 04:22:55 +0000
@@ -0,0 +1,17 @@
1This branch is based off lp:~bjornt/lp2kanban/landscape and contains the
2configuration files needed for lp2kanban to process bugs for the Landscape
3project.
4
5To deploy, first run ./create_creds.py to create the credentials file. You
6should be logged into Launchpad as the lp2kanban user when doing this, unless
7for testing.
8
9Next you need to add cron entries to run the bugs2kanban.py script. You will
10need to run it twice, one for creating new bugs (create.ini) and one for
11syncing the card information with bugs (sync.ini):
12
13 bin/py src/lp2kanban/bugs2kanban.py -c create.ini
14 bin/py src/lp2kanban/bugs2kanban.py -c sync.ini
15
16The former is quite quick and can be run every minute. The latter is slower
17and shouldn't be run more often than every 5 minutes.
018
=== modified file 'bootstrap.py'
--- bootstrap.py 2013-03-18 15:45:56 +0000
+++ bootstrap.py 2015-12-16 04:22:55 +0000
@@ -18,7 +18,11 @@
18use the -c option to specify an alternate configuration file.18use the -c option to specify an alternate configuration file.
19"""19"""
2020
21import os, shutil, sys, tempfile21import os
22import shutil
23import sys
24import tempfile
25
22from optparse import OptionParser26from optparse import OptionParser
2327
24tmpeggs = tempfile.mkdtemp()28tmpeggs = tempfile.mkdtemp()
@@ -31,8 +35,8 @@
31Simply run this script in a directory containing a buildout.cfg, using the35Simply run this script in a directory containing a buildout.cfg, using the
32Python that you want bin/buildout to use.36Python that you want bin/buildout to use.
3337
34Note that by using --setup-source and --download-base to point to38Note that by using --find-links to point to local resources, you can keep
35local resources, you can keep this script from going over the network.39this script from going over the network.
36'''40'''
3741
38parser = OptionParser(usage=usage)42parser = OptionParser(usage=usage)
@@ -48,48 +52,63 @@
48 "bootstrap and buildout will get the newest releases "52 "bootstrap and buildout will get the newest releases "
49 "even if they are alphas or betas."))53 "even if they are alphas or betas."))
50parser.add_option("-c", "--config-file",54parser.add_option("-c", "--config-file",
51 help=("Specify the path to the buildout configuration "55 help=("Specify the path to the buildout configuration "
52 "file to be used."))56 "file to be used."))
53parser.add_option("-f", "--find-links",57parser.add_option("-f", "--find-links",
54 help=("Specify a URL to search for buildout releases"))58 help=("Specify a URL to search for buildout releases"))
59parser.add_option("--allow-site-packages",
60 action="store_true", default=False,
61 help=("Let bootstrap.py use existing site packages"))
62parser.add_option("--setuptools-version",
63 help="use a specific setuptools version")
5564
5665
57options, args = parser.parse_args()66options, args = parser.parse_args()
5867
59######################################################################68######################################################################
60# load/install distribute69# load/install setuptools
6170
62to_reload = False
63try:71try:
64 import pkg_resources, setuptools72 if options.allow_site_packages:
65 if not hasattr(pkg_resources, '_distribute'):73 import setuptools
66 to_reload = True74 import pkg_resources
67 raise ImportError75 from urllib.request import urlopen
68except ImportError:76except ImportError:
69 ez = {}77 from urllib2 import urlopen
7078
71 try:79ez = {}
72 from urllib.request import urlopen80exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
73 except ImportError:81
74 from urllib2 import urlopen82if not options.allow_site_packages:
7583 # ez_setup imports site, which adds site packages
76 exec(urlopen('http://python-distribute.org/distribute_setup.py').read(), ez)84 # this will remove them from the path to ensure that incompatible versions
77 setup_args = dict(to_dir=tmpeggs, download_delay=0, no_fake=True)85 # of setuptools are not in the path
78 ez['use_setuptools'](**setup_args)86 import site
7987 # inside a virtualenv, there is no 'getsitepackages'.
80 if to_reload:88 # We can't remove these reliably
81 reload(pkg_resources)89 if hasattr(site, 'getsitepackages'):
82 import pkg_resources90 for sitepackage_path in site.getsitepackages():
83 # This does not (always?) update the default working set. We will91 sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
84 # do it.92
85 for path in sys.path:93setup_args = dict(to_dir=tmpeggs, download_delay=0)
86 if path not in pkg_resources.working_set.entries:94
87 pkg_resources.working_set.add_entry(path)95if options.setuptools_version is not None:
96 setup_args['version'] = options.setuptools_version
97
98ez['use_setuptools'](**setup_args)
99import setuptools
100import pkg_resources
101
102# This does not (always?) update the default working set. We will
103# do it.
104for path in sys.path:
105 if path not in pkg_resources.working_set.entries:
106 pkg_resources.working_set.add_entry(path)
88107
89######################################################################108######################################################################
90# Install buildout109# Install buildout
91110
92ws = pkg_resources.working_set111ws = pkg_resources.working_set
93112
94cmd = [sys.executable, '-c',113cmd = [sys.executable, '-c',
95 'from setuptools.command.easy_install import main; main()',114 'from setuptools.command.easy_install import main; main()',
@@ -104,8 +123,8 @@
104if find_links:123if find_links:
105 cmd.extend(['-f', find_links])124 cmd.extend(['-f', find_links])
106125
107distribute_path = ws.find(126setuptools_path = ws.find(
108 pkg_resources.Requirement.parse('distribute')).location127 pkg_resources.Requirement.parse('setuptools')).location
109128
110requirement = 'zc.buildout'129requirement = 'zc.buildout'
111version = options.version130version = options.version
@@ -113,13 +132,19 @@
113 # Figure out the most recent final version of zc.buildout.132 # Figure out the most recent final version of zc.buildout.
114 import setuptools.package_index133 import setuptools.package_index
115 _final_parts = '*final-', '*final'134 _final_parts = '*final-', '*final'
135
116 def _final_version(parsed_version):136 def _final_version(parsed_version):
117 for part in parsed_version:137 try:
118 if (part[:1] == '*') and (part not in _final_parts):138 return not parsed_version.is_prerelease
119 return False139 except AttributeError:
120 return True140 # Older setuptools
141 for part in parsed_version:
142 if (part[:1] == '*') and (part not in _final_parts):
143 return False
144 return True
145
121 index = setuptools.package_index.PackageIndex(146 index = setuptools.package_index.PackageIndex(
122 search_path=[distribute_path])147 search_path=[setuptools_path])
123 if find_links:148 if find_links:
124 index.add_find_links((find_links,))149 index.add_find_links((find_links,))
125 req = pkg_resources.Requirement.parse(requirement)150 req = pkg_resources.Requirement.parse(requirement)
@@ -142,10 +167,9 @@
142cmd.append(requirement)167cmd.append(requirement)
143168
144import subprocess169import subprocess
145if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=distribute_path)) != 0:170if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
146 raise Exception(171 raise Exception(
147 "Failed to execute command:\n%s",172 "Failed to execute command:\n%s" % repr(cmd)[1:-1])
148 repr(cmd)[1:-1])
149173
150######################################################################174######################################################################
151# Import and run buildout175# Import and run buildout
152176
=== modified file 'buildout.cfg'
--- buildout.cfg 2013-06-19 19:44:20 +0000
+++ buildout.cfg 2015-12-16 04:22:55 +0000
@@ -11,6 +11,7 @@
11exec-sitecustomize = true11exec-sitecustomize = true
12develop = .12develop = .
13eggs-directory = eggs13eggs-directory = eggs
14offline = false
1415
15[test]16[test]
16recipe = zc.recipe.testrunner17recipe = zc.recipe.testrunner
@@ -38,13 +39,12 @@
38lazr.restfulclient = 0.11.239lazr.restfulclient = 0.11.2
39lazr.uri = 1.0.240lazr.uri = 1.0.2
40oauth = 1.0.141oauth = 1.0.1
41setuptools = 0.6c11
42simplejson = 2.1.642simplejson = 2.1.6
43wadllib = 1.2.043wadllib = 1.2.0
44wsgi-intercept = 0.5.044wsgi-intercept = 0.5.0
45z3c.recipe.scripts = 1.0.145z3c.recipe.scripts = 1.0.1
46z3c.recipe.tag = 0.4.046z3c.recipe.tag = 0.4.0
47zc.buildout = 1.5.247zc.buildout = 1.7.1
48zc.recipe.egg = 1.3.248zc.recipe.egg = 1.3.2
49zc.recipe.testrunner = 1.4.049zc.recipe.testrunner = 1.4.0
50zope.exceptions = 3.6.150zope.exceptions = 3.6.1
5151
=== added directory 'configs'
=== added file 'configs/groups.ini'
--- configs/groups.ini 1970-01-01 00:00:00 +0000
+++ configs/groups.ini 2015-12-16 04:22:55 +0000
@@ -0,0 +1,16 @@
1# The group name, which will replace ${group} in lane configs.
2# The Launchpad account names of the group members.
3[Alpha]
4members = benji,simpoir
5[Beta]
6members = fcorrea,chad.smith
7[Gamma]
8members = adam-collard,bogdana
9[Delta]
10members = tealeg,danilo
11[Epsilon]
12members = free.ekanayaka,ack
13[Zeta]
14members = bjornt,tribaal
15[QA]
16members = ahasenack
017
=== added file 'configs/sync.ini'
--- configs/sync.ini 1970-01-01 00:00:00 +0000
+++ configs/sync.ini 2015-12-16 04:22:55 +0000
@@ -0,0 +1,106 @@
1[GLOBAL]
2# This is the account part of https://<account>.leankitkanban.com/ URL.
3account = canonical
4# LeankitKanban user name and password.
5user = landscape-crew@lists.canonical.com
6password = fShv0K4h@LV:!{
7
8# Defaults are common values overridable in each of the boards definition.
9[DEFAULT]
10lp_server = production
11launchpadlib_dir = .launchpadlib.create
12
13# Should all cards with external IDs be synced or not. Set to 'on' if
14# you want them synced automatically (and use "(no-sync)" as the marker
15# for cards you don't want synced).
16autosync = on
17
18# Should the cards be moved? If not specified it is off.
19move_cards = on
20
21# Should the cards be synced? If not specified it is off.
22sync_cards = on
23
24# Bug tag for which new cards should be created. If a bug in Launchpad
25# has this tag, a new card will be created for it in the first kanban lane.
26# If this config option isn't set, no cards will be created.
27# After the card has been created, the tag will be removed from the bug, so a
28# credentials_file has to be specified as well, to authenticate against LP.
29bug_to_card_tag = kanban
30
31# Which card type should cards created from bugs have. If not set, the default
32# card type is used.
33bug_to_card_type = Bug
34
35bug_to_card_lane = Backlog
36
37# By default, omnidirectional card moves are off, which means that cards will
38# be moved only to the lane that is considered to be next of the card's
39# current lane. I.e. a card will move only within a "swim lane". If
40# omnidirectional card moves are turned on, a card can move to any lane
41# on the board.
42omnidirectional_card_moves = on
43
44# Lanes are assigned a "role" by looking for sublanes matching
45# values given here. "Coding" will match all lanes which have
46# a title of "Coding", and "QA::Ready" will match all lanes
47# titled "Ready" that have a parent lane with title "QA".
48#
49# Note that bugs2cards.py analyzes the lane structure and will flow
50# cards only within the same horizontal "lane" (iow, if you have
51# parallel 'Feature' and 'Bugs' development lanes, cards will flow
52# only from Coding -> Review inside them, and then will move to the
53# next lane horizontally (eg. Landing lane).
54#coding_lanes =
55#review_lanes =
56#landing_lanes =
57#qa_lanes =
58#deploy_lanes =
59#downtime_deploy_lanes =
60#done_lanes =
61
62
63# If you track multiple groups of people on one board, you can specify a
64# groups config that will map a card to a group according to its
65#assignee. You can then make the lane configs to include the group
66# name, by using ${group} in the lane name. For example:
67#
68# coding_lanes = ${group}::Coding
69#
70groups_config_file = ./configs/groups.ini
71
72credentials_file = ./credentials
73
74
75#[Landscape Cisco]
76# Keep [Landscape Cisco] board before [Landscape] so it gets the "falkor" cards
77# (we can only use external ID for one card across all of Leankit).
78#projects = falkor
79
80#bug_to_card_lane = Backlog
81
82#coding_lanes = Doing
83#review_lanes = Doing
84#landing_lanes = Done
85#deploy_lanes = Done
86#done_lanes = Done
87
88#no_move_lanes =
89#no_sync_lanes =
90
91
92[Landscape 2016]
93project_group = landscape-project
94
95# TODO: This should put charm bugs in ::Charm and server/client bugs
96# in ::Landscape, for now stick everything in ::Landscape
97bug_to_card_lane = Backlog::Engineering
98
99coding_lanes = Development::${group}::Doing
100review_lanes = Development::${group}::Review
101landing_lanes = Development::${group}::Landed
102deploy_lanes = QA::Landed
103done_lanes = Done
104
105no_move_lanes = Done,QA::Landed,QA::In progress
106no_sync_lanes = Backlog::Engineering,Done,QA::Landed,QA::In progress,Archive,Ready for QA,Release Ready,Staging,Production,LDS
0107
=== modified file 'create_creds.py'
--- create_creds.py 2012-12-10 15:21:44 +0000
+++ create_creds.py 2015-12-16 04:22:55 +0000
@@ -7,7 +7,7 @@
77
8web_root="https://launchpad.net/"8web_root="https://launchpad.net/"
99
10creds = Credentials("tarmac")10creds = Credentials("lp2kanban-landscape")
11url = creds.get_request_token(web_root=web_root)11url = creds.get_request_token(web_root=web_root)
1212
13subprocess.call(['xdg-open', url])13subprocess.call(['xdg-open', url])
1414
=== added file 'jenkins.sh'
--- jenkins.sh 1970-01-01 00:00:00 +0000
+++ jenkins.sh 2015-12-16 04:22:55 +0000
@@ -0,0 +1,32 @@
1#!/bin/bash -xe
2
3ini="$1.ini"
4date "+START: %c"
5
6#export https_proxy=http://squid.external:3128
7#export http_proxy=http://squid.external:3128
8
9make
10
11if [ ! -e credentials ]; then
12 if [ -e ~/credentials ]; then
13 cp ~/credentials credentials
14 else
15 echo "ERROR: try make credentials first!"
16 exit 1
17 fi
18fi
19
20# default encoding of stdout/stderr when not attached to a terminal is set to
21# 'None'. 'PYTHONIOENCODING' overrides, could also be overridden in the
22# python program by modifying 'sys.stdout.encoding' directly. This feels
23# cleaner. Note setting LANG (which the terminal respects) wont fix this,
24# since there is no terminal attached.
25# --------
26# See:
27# - https://wiki.python.org/moin/PrintFails
28# - https://stackoverflow.com/questions/1473577
29# - https://stackoverflow.com/questions/492483
30# - https://stackoverflow.com/questions/9932406
31PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini
32date "+END: %c"
033
=== added directory 'logs'
=== added file 'run.sh'
--- run.sh 1970-01-01 00:00:00 +0000
+++ run.sh 2015-12-16 04:22:55 +0000
@@ -0,0 +1,21 @@
1#!/bin/bash
2
3LOGFILE="logs/${1}-`date '+%Y-%m-%d'`.log"
4LOCKFILE="$HOME/lp2kanban/run-$1.lock"
5exit_status=0
6export https_proxy=http://squid.external:3128
7
8cd $HOME/lp2kanban
9echo -n "START " >> $LOGFILE
10date "+%c" >> $LOGFILE
11lockfile -10 -r 4 $LOCKFILE
12if [ "$?" -eq 0 ]; then
13 bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini 2>&1 >> $LOGFILE
14 exit_status=$?
15 rm -f $LOCKFILE
16else
17 echo "Failed to grab lockfile $LOCKFILE" >> $LOGFILE
18 exit_status=1
19fi
20echo -n "END " >> $LOGFILE; date "+%c" >> $LOGFILE
21exit $?
022
=== modified file 'src/lp2kanban/bugs2cards.py'
--- src/lp2kanban/bugs2cards.py 2013-06-19 19:44:20 +0000
+++ src/lp2kanban/bugs2cards.py 2015-12-16 04:22:55 +0000
@@ -1,4 +1,4 @@
1# Copyright 2011 Canonical Ltd1# Copyright 2015 Canonical Ltd
2#2#
3from ConfigParser import ConfigParser3from ConfigParser import ConfigParser
4from argparse import ArgumentParser4from argparse import ArgumentParser
@@ -9,6 +9,7 @@
9 update_blueprints_from_work_items,9 update_blueprints_from_work_items,
10 )10 )
11from lp2kanban.kanban import (11from lp2kanban.kanban import (
12 BRANCH_REGEX,
12 LeankitKanban,13 LeankitKanban,
13 Record,14 Record,
14 )15 )
@@ -117,6 +118,9 @@
117 for user in board.users:118 for user in board.users:
118 lp_user = lp.people.getByEmail(email=user)119 lp_user = lp.people.getByEmail(email=user)
119 if lp_user is None:120 if lp_user is None:
121 print (
122 "WARNING: There is no matching Launchpad user for kanban"
123 " user %s." % (user,))
120 continue124 continue
121 self.lp_to_kanban[lp_user.name] = board.users[user]125 self.lp_to_kanban[lp_user.name] = board.users[user]
122 self.kanban_to_lp[board.users[user]] = lp_user126 self.kanban_to_lp[board.users[user]] = lp_user
@@ -161,8 +165,10 @@
161165
162 def _get_mp_info():166 def _get_mp_info():
163 mps = []167 mps = []
164 for bug_branch in branches:168 for branch in branches:
165 for mp in bug_branch.branch.landing_targets:169 if hasattr(branch, 'branch') :
170 branch = branch.branch
171 for mp in branch.landing_targets:
166 mp_info = Record(rank=None, status=None, mp=None)172 mp_info = Record(rank=None, status=None, mp=None)
167 status = mp.queue_status173 status = mp.queue_status
168 mp_info.rank = ORDERED_STATUSES.get(status, 1)174 mp_info.rank = ORDERED_STATUSES.get(status, 1)
@@ -222,8 +228,10 @@
222 if conf.get('autosync', None) == 'on':228 if conf.get('autosync', None) == 'on':
223 has_id = (card.external_card_id is not None and229 has_id = (card.external_card_id is not None and
224 card.external_card_id.strip() != '')230 card.external_card_id.strip() != '')
225 return (has_id and NO_SYNC_MARKER not in card.title and231 has_branch = BRANCH_REGEX.match(card.external_system_url)
226 NO_SYNC_MARKER not in card.description)232 if NO_SYNC_MARKER in card.title or NO_SYNC_MARKER in card.description:
233 return False
234 return (has_id or has_branch)
227 else:235 else:
228 return (card.title.startswith(TITLE_MARKER) or236 return (card.title.startswith(TITLE_MARKER) or
229 DESCRIPTION_MARKER in card.description)237 DESCRIPTION_MARKER in card.description)
@@ -296,15 +304,21 @@
296 """Return the status of the card as one of CardStatus values."""304 """Return the status of the card as one of CardStatus values."""
297 # status of None means card needs not to be moved.305 # status of None means card needs not to be moved.
298 status = None306 status = None
299 if bug_status in IN_PROGRESS_BUG_STATUSES:307 branch_card_status = None
300 status = CardStatus.CODING308 if branch_info:
301 if branch_info.status == 'In Progress':309 if branch_info.status == 'In Progress':
302 status = CardStatus.CODING310 branch_card_status = CardStatus.CODING
303 elif branch_info.status == 'In Review':311 elif branch_info.status == 'In Review':
304 status = CardStatus.REVIEW312 branch_card_status = CardStatus.REVIEW
305 elif branch_info.status == 'Approved':313 elif branch_info.status == 'Approved':
306 status = CardStatus.LANDING314 branch_card_status = CardStatus.LANDING
307 elif branch_info.status == 'Merged':315 elif branch_info.status == 'Merged':
316 branch_card_status = CardStatus.LANDING
317 if bug_status is None:
318 return branch_card_status
319 if bug_status in IN_PROGRESS_BUG_STATUSES:
320 status = CardStatus.CODING
321 if branch_info.status == 'Merged':
308 if bug_status == 'Fix Committed':322 if bug_status == 'Fix Committed':
309 if 'qa-needstesting' in bug_tags:323 if 'qa-needstesting' in bug_tags:
310 status = CardStatus.QA324 status = CardStatus.QA
@@ -315,6 +329,8 @@
315 status = CardStatus.DEPLOY329 status = CardStatus.DEPLOY
316 else:330 else:
317 status = CardStatus.LANDING331 status = CardStatus.LANDING
332 elif branch_card_status:
333 return branch_card_status
318 elif bug_status in DONE_BUG_STATUSES:334 elif bug_status in DONE_BUG_STATUSES:
319 status = CardStatus.DONE335 status = CardStatus.DONE
320 return status336 return status
@@ -454,6 +470,37 @@
454 board, project, new_bug_tag, bconf.get("bug_to_card_type"),470 board, project, new_bug_tag, bconf.get("bug_to_card_type"),
455 bconf.get("bug_to_card_lane"))471 bconf.get("bug_to_card_lane"))
456 print " Syncing cards:"472 print " Syncing cards:"
473 # Process linked branch cards
474 for card in board.getCardsWithExternalLinks(only_branches=True):
475 if card.external_card_id:
476 # Cards with external_id are processed by getCardsWithExternalIds
477 continue
478 branch_name = card.external_system_url.replace(
479 "https://code.launchpad.net/", "")
480 branch = lp.branches.getByUniqueName(unique_name=branch_name)
481 if not branch:
482 print "Invalid branch url ({}) for card '{}'".format(
483 card.external_system_url, card.title)
484 continue
485 if should_sync_card(card, bconf):
486 branch_info = get_branch_info([branch])
487 owner_name = BRANCH_REGEX.match(card.external_system_url).group(1)
488 card_status = get_card_status(None, '', branch_info)
489 if branch_info.status and should_move_card(card, bconf):
490 assignee = Record(name=owner_name)
491 try:
492 move_card(card, card_status, bconf, [assignee], lp_users)
493 except IOError as e:
494 print " * %s (in %s)" % (card.title, card.lane.path)
495 print " >>> Error moving card:"
496 print " ", e
497 continue
498 kanban_user = lp_users.lp_to_kanban.get(assignee.name, None)
499 if kanban_user:
500 card.assigned_user_id = kanban_user.id
501 card.kanban_user = kanban_user
502 print " * %s (in %s)" % (card.title, card.lane.path)
503
457 for card in board.getCardsWithExternalIds():504 for card in board.getCardsWithExternalIds():
458 if should_sync_card(card, bconf):505 if should_sync_card(card, bconf):
459 try:506 try:
460507
=== modified file 'src/lp2kanban/kanban.py'
--- src/lp2kanban/kanban.py 2013-05-24 14:48:07 +0000
+++ src/lp2kanban/kanban.py 2015-12-16 04:22:55 +0000
@@ -11,6 +11,8 @@
1111
1212
13ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL)13ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL)
14BRANCH_REGEX = re.compile(
15 '^https://code.launchpad.net/~([.\w]*)/([-\w]*)/([-\w]*)$')
1416
1517
16class Record(dict):18class Record(dict):
@@ -55,8 +57,7 @@
5557
56class LeankitConnector(object):58class LeankitConnector(object):
57 def __init__(self, account, username=None, password=None, throttle=1):59 def __init__(self, account, username=None, password=None, throttle=1):
58 host = 'https://' + account + '.leankitkanban.com'60 self.base_api_url = 'https://' + account + '.leankit.com'
59 self.base_api_url = host + '/Kanban/Api'
60 self.http = self._configure_auth(username, password)61 self.http = self._configure_auth(username, password)
61 self.last_request_time = time.time() - throttle62 self.last_request_time = time.time() - throttle
62 self.throttle = throttle63 self.throttle = throttle
@@ -97,6 +98,10 @@
97 config={},98 config={},
98 return_response=False)99 return_response=False)
99 sent = request.send()100 sent = request.send()
101 except AttributeError as e:
102 # Weirdly, httplib2 has a habit of throwing an AttributeError
103 # when it can't connect, so we handle that nicely.
104 raise IOError("Unable to connect to LeanKitKanban server.")
100 except Exception as e:105 except Exception as e:
101 raise IOError("Unable to make HTTP request: %s" % e.message)106 raise IOError("Unable to make HTTP request: %s" % e.message)
102107
@@ -193,14 +198,17 @@
193 optional_attributes = [198 optional_attributes = [
194 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked',199 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked',
195 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl',200 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl',
196 'ClassOfServiceId', 'DueDate',201 'ClassOfServiceId', 'DueDate', 'CurrentTaskBoardId'
197 ]202 ]
198203
199 def __init__(self, card_dict, lane):204 def __init__(self, card_dict, lane):
200 super(LeankitCard, self).__init__(card_dict)205 super(LeankitCard, self).__init__(card_dict)
201206
202 self.lane = lane207 self.lane = lane
203 self.tags_list = set([tag.strip() for tag in self.tags.split(',')])208 if not self.tags:
209 self.tags_list = []
210 else:
211 self.tags_list = set([tag.strip() for tag in self.tags.split(',')])
204 if '' in self.tags_list:212 if '' in self.tags_list:
205 self.tags_list.remove('')213 self.tags_list.remove('')
206 self.type = lane.board.cardtypes[self.type_id]214 self.type = lane.board.cardtypes[self.type_id]
@@ -214,7 +222,7 @@
214 tag = tag.strip()222 tag = tag.strip()
215 if tag not in self.tags_list or self.tags.startswith(','):223 if tag not in self.tags_list or self.tags.startswith(','):
216 if tag != '':224 if tag != '':
217 self.tags_list.add(tag)225 self.tags_list.append(tag)
218 self.tags = ', '.join(self.tags_list)226 self.tags = ', '.join(self.tags_list)
219227
220 def save(self):228 def save(self):
@@ -241,10 +249,12 @@
241 del data['Id']249 del data['Id']
242 del data['LaneId']250 del data['LaneId']
243 position = len(self.lane.cards)251 position = len(self.lane.cards)
244 url_parts = ['/Board', str(self.lane.board.id), 'AddCard',252 url_parts = ['/Kanban/Api/Board', str(self.lane.board.id),
245 'Lane', str(self.lane.id), 'Position', str(position)]253 'AddCard', 'Lane', str(self.lane.id),
254 'Position', str(position)]
246 else:255 else:
247 url_parts = ['/Board', str(self.lane.board.id), 'UpdateCard']256 url_parts = ['/Kanban/Api/Board', str(self.lane.board.id),
257 'UpdateCard']
248258
249 url = '/'.join(url_parts)259 url = '/'.join(url_parts)
250260
@@ -267,9 +277,23 @@
267 else:277 else:
268 return None278 return None
269279
280 def moveToTaskBoard(self, parent_board, target_card):
281 url = '/Api/Card/MoveCardToTaskboard'
282 result = self.lane.board.connector.post(url, data={
283 'boardId': parent_board.id,
284 'destCardId': target_card.id,
285 'srcCardId': [self.id]})
286 if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
287 return result.ReplyData[0]
288 else:
289 raise Exception(
290 "Moving card %s (%s) to %s failed. " % (
291 self.title, self.id, self.lane.path) +
292 "Error %s: %s" % (result.ReplyCode, result.ReplyText))
293
270 def _moveCard(self):294 def _moveCard(self):
271 target_pos = len(self.lane.cards)295 target_pos = len(self.lane.cards)
272 url = '/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % (296 url = '/Kanban/Api/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % (
273 self.lane.board.id, self.id, self.lane.id, target_pos)297 self.lane.board.id, self.id, self.lane.id, target_pos)
274 result = self.lane.board.connector.post(url, data=None)298 result = self.lane.board.connector.post(url, data=None)
275 if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:299 if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
@@ -323,6 +347,8 @@
323 text_after_json), where json_annotations contains the347 text_after_json), where json_annotations contains the
324 JSON loaded with json.loads().348 JSON loaded with json.loads().
325 """349 """
350 if not self.description:
351 self.description = ""
326 match = ANNOTATION_REGEX.search(self.description)352 match = ANNOTATION_REGEX.search(self.description)
327 if match:353 if match:
328 start = match.start()354 start = match.start()
@@ -330,7 +356,7 @@
330 try:356 try:
331 annotations = Record(json.loads(self.description[start:end]))357 annotations = Record(json.loads(self.description[start:end]))
332 except ValueError, ex:358 except ValueError, ex:
333 print "Unable to parse card %i: %s" % (self.id, ex.message)359 print "Unable to parse card %s: %s" % (self.id, ex.message)
334 annotations = Record()360 annotations = Record()
335 return (361 return (
336 annotations,362 annotations,
@@ -450,7 +476,7 @@
450476
451 attributes = ['Id', 'Title', 'CreationDate', 'IsArchived']477 attributes = ['Id', 'Title', 'CreationDate', 'IsArchived']
452478
453 base_uri = '/Boards/'479 base_uri = '/Kanban/Api/Boards/'
454480
455 def __init__(self, board_dict, connector):481 def __init__(self, board_dict, connector):
456 super(LeankitBoard, self).__init__(board_dict)482 super(LeankitBoard, self).__init__(board_dict)
@@ -469,12 +495,20 @@
469 self._cards_with_external_ids = set()495 self._cards_with_external_ids = set()
470 self._cards_with_description_annotations = set()496 self._cards_with_description_annotations = set()
471 self._cards_with_external_links = set()497 self._cards_with_external_links = set()
498 self._cards_with_branches = set()
472 self.default_cardtype = None499 self.default_cardtype = None
473500
474 def getCardsWithExternalIds(self):501 def getCardsWithExternalIds(self):
475 return self._cards_with_external_ids502 return self._cards_with_external_ids
476503
477 def getCardsWithExternalLinks(self):504 def getCardsWithExternalLinks(self, only_branches=False):
505 """Return cards with external links
506
507 @param only_merge_proposals: Only return cards that have merge
508 proposals specified as the card's external link
509 """
510 if only_branches:
511 return self._cards_with_branches
478 return self._cards_with_external_links512 return self._cards_with_external_links
479513
480 def getCardsWithDescriptionAnnotations(self):514 def getCardsWithDescriptionAnnotations(self):
@@ -487,15 +521,23 @@
487 self._populateUsers(self.details['BoardUsers'])521 self._populateUsers(self.details['BoardUsers'])
488 self._populateCardTypes(self.details['CardTypes'])522 self._populateCardTypes(self.details['CardTypes'])
489 self._archive = self.connector.get(523 self._archive = self.connector.get(
490 "/Board/" + str(self.id) + "/Archive").ReplyData[0]524 "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]
491 archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]525 archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
492 archive_lanes.extend(526 archive_lanes.extend(
493 [lane_dict['Lane'] for527 [lane_dict['Lane'] for
494 lane_dict in self._archive[0]['ChildLanes']])528 lane_dict in self._archive[0]['ChildLanes']])
495 self._backlog = self.connector.get(529 self._backlog = self.connector.get(
496 "/Board/" + str(self.id) + "/Backlog").ReplyData[0]530 "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0]
497 self._populateLanes(531 self._populateLanes(
498 self.details['Lanes'] + archive_lanes + self._backlog)532 self.details['Lanes'] + archive_lanes + self._backlog)
533 for card in self.cards:
534 if card.current_task_board_id: # We are a task board
535 taskboard_data = self.connector.get(
536 "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
537 card.taskboard = LeankitTaskBoard(
538 taskboard_data["ReplyData"][0], self)
539 card.taskboard.fetchDetails()
540 self.cards.extend(card.taskboard.cards)
499 self._classifyCards()541 self._classifyCards()
500542
501 def _classifyCards(self):543 def _classifyCards(self):
@@ -508,12 +550,16 @@
508 self._cards_with_description_annotations.add(card)550 self._cards_with_description_annotations.add(card)
509 if card.external_system_url:551 if card.external_system_url:
510 self._cards_with_external_links.add(card)552 self._cards_with_external_links.add(card)
553 if BRANCH_REGEX.match(card.external_system_url):
554 self._cards_with_branches.add(card)
511 print " - %s cards with external ids" % len(555 print " - %s cards with external ids" % len(
512 self._cards_with_external_ids)556 self._cards_with_external_ids)
513 print " - %s cards with external links" % len(557 print " - %s cards with external links" % len(
514 self._cards_with_external_links)558 self._cards_with_external_links)
515 print " - %s cards with description annotations" % len(559 print " - %s cards with description annotations" % len(
516 self._cards_with_description_annotations)560 self._cards_with_description_annotations)
561 print " - %s cards with branches" % len(
562 self._cards_with_branches)
517563
518 def _populateUsers(self, user_data):564 def _populateUsers(self, user_data):
519 self.users = {}565 self.users = {}
@@ -625,6 +671,21 @@
625 self._printLanes(lane, indent, include_cards)671 self._printLanes(lane, indent, include_cards)
626672
627673
674class LeankitTaskBoard(LeankitBoard):
675
676 attributes = ['Id', 'Title']
677
678 def __init__(self, taskboard_dict, board):
679 super(LeankitTaskBoard, self).__init__(taskboard_dict, board.connector)
680 self.base_uri = '/Api/Board/%d/TaskBoard/%s/Get' % (board.id, self.id)
681 self.cardtypes = board.cardtypes
682 self.parent_board = board
683
684 def fetchDetails(self):
685 self.details = self.connector.get(self.base_uri).ReplyData[0]
686 self._populateLanes(self.details['Lanes'])
687
688
628class LeankitKanban(object):689class LeankitKanban(object):
629690
630 def __init__(self, account, username=None, password=None):691 def __init__(self, account, username=None, password=None):
@@ -638,7 +699,7 @@
638699
639 :param include_archived: if True, include archived boards as well.700 :param include_archived: if True, include archived boards as well.
640 """701 """
641 boards_data = self.connector.get('/Boards').ReplyData702 boards_data = self.connector.get('/Kanban/Api/Boards').ReplyData
642 boards = []703 boards = []
643 for board_dict in boards_data[0]:704 for board_dict in boards_data[0]:
644 board = LeankitBoard(board_dict, self.connector)705 board = LeankitBoard(board_dict, self.connector)
@@ -683,13 +744,14 @@
683if __name__ == '__main__':744if __name__ == '__main__':
684 kanban = LeankitKanban('launchpad.leankitkanban.com',745 kanban = LeankitKanban('launchpad.leankitkanban.com',
685 'user@email', 'password')746 'user@email', 'password')
747
686 print "Active boards:"748 print "Active boards:"
687 boards = kanban.getBoards()749 boards = kanban.getBoards()
688 for board in boards:750 for board in boards:
689 print " * %s (%d)" % (board.title, board.id)751 print " * %s (%d)" % (board.title, board.id)
690752
691 # Get a board by the title.753 # Get a board by the title.
692 board_name = 'lp2kanban test'754 board_name = 'Landscape 2016'
693 print "Getting board '%s'..." % board_name755 print "Getting board '%s'..." % board_name
694 board = kanban.getBoard(title=board_name)756 board = kanban.getBoard(title=board_name)
695 board.printLanes()757 board.printLanes()
696758
=== modified file 'src/lp2kanban/tests/common.py'
--- src/lp2kanban/tests/common.py 2013-04-08 12:46:05 +0000
+++ src/lp2kanban/tests/common.py 2015-12-16 04:22:55 +0000
@@ -33,13 +33,16 @@
33 self.is_archived = is_archived33 self.is_archived = is_archived
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.lanes = {}37 self.lanes = {}
37 self.root_lane = self.addLane('ROOT LANE')38 self.root_lane = self.addLane('ROOT LANE')
3839
39 def getCardsWithDescriptionAnnotations(self):40 def getCardsWithDescriptionAnnotations(self):
40 return self._cards_with_description_annotations41 return self._cards_with_description_annotations
4142
42 def getCardsWithExternalLinks(self):43 def getCardsWithExternalLinks(self, only_branches=False):
44 if only_branches:
45 return self._cards_with_branches
43 return self._cards_with_external_links46 return self._cards_with_external_links
4447
45 def getLaneByPath(self, path):48 def getLaneByPath(self, path):
@@ -93,7 +96,7 @@
93 def __init__(self, external_card_id=None, title=u"", description=u"",96 def __init__(self, external_card_id=None, title=u"", description=u"",
94 description_annotations=None, lane=None,97 description_annotations=None, lane=None,
95 assigned_user_id=None, external_system_name=None,98 assigned_user_id=None, external_system_name=None,
96 external_system_url=None):99 external_system_url=u""):
97 self.external_card_id = external_card_id100 self.external_card_id = external_card_id
98 self.title = title101 self.title = title
99 self.description = description102 self.description = description
100103
=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
--- src/lp2kanban/tests/test_bugs2cards.py 2013-04-08 12:50:46 +0000
+++ src/lp2kanban/tests/test_bugs2cards.py 2015-12-16 04:22:55 +0000
@@ -346,6 +346,7 @@
346 # in CODING.346 # in CODING.
347 self.assertEqual(CardStatus.CODING, status)347 self.assertEqual(CardStatus.CODING, status)
348348
349BRANCH_URL = "https://code.launchpad.net/~me/project/branch-name"
349350
350class CardStatusTest(unittest.TestCase):351class CardStatusTest(unittest.TestCase):
351352
@@ -375,23 +376,43 @@
375376
376 def test_should_sync_card_autosync_no(self):377 def test_should_sync_card_autosync_no(self):
377 # When autosync is 'on', cards with no external card IDs378 # When autosync is 'on', cards with no external card IDs
378 # are not synced.379 # and no branches are not synced.
379 card = Record(title=u'no sync', description=u'',380 card = Record(title=u'no sync', description=u'',
380 external_card_id=None)381 external_card_id=None, external_system_url=None)
381 self.assertFalse(should_sync_card(card, {'sync_cards': 'on'}))382 self.assertFalse(should_sync_card(card, {'sync_cards': 'on'}))
382383
383 def test_should_sync_card_autosync_yes(self):384 def test_should_sync_card_autosync_no_sync_with_non_branch_urls(self):
385 # When autosync is 'on', cards with external_system_urls which are not
386 # valid launchpad branches are not synced.
387 non_branch_urls = [
388 "http://www.google.com/",
389 "https://bugs.launchpad.net/charms/+source/hacluster/+bug/1",
390 "https://code.launchpad.net/~person/project/blah/+merge/111"
391 ]
392 for url in non_branch_urls:
393 card = Record(title=u'no sync', description=u'',
394 external_card_id=None, external_system_url=url)
395 self.assertFalse(should_sync_card(card, {'sync_cards': 'on'}))
396
397 def test_should_sync_card_autosync_synced_with_external_card_id(self):
384 # When autosync is 'on', cards with external card IDs are synced.398 # When autosync is 'on', cards with external card IDs are synced.
385 card = Record(title=u'no sync', description=u'',399 card = Record(title=u'no sync', description=u'',
386 external_card_id='11')400 external_card_id=u'11', external_system_url=u'')
401 self.assertTrue(should_sync_card(card, {'autosync': 'on',
402 'sync_cards': 'on'}))
403
404 def test_should_sync_card_autosync_synced_with_branch(self):
405 # When autosync is 'on', cards with a valid branch are synced.
406 card = Record(title=u'no sync', description=u'',
407 external_card_id=u'11', external_system_url=BRANCH_URL)
387 self.assertTrue(should_sync_card(card, {'autosync': 'on',408 self.assertTrue(should_sync_card(card, {'autosync': 'on',
388 'sync_cards': 'on'}))409 'sync_cards': 'on'}))
389410
390 def test_should_sync_card_autosync_nosync(self):411 def test_should_sync_card_autosync_nosync(self):
391 # When autosync is 'on', cards with external card IDs are not412 # When autosync is 'on', cards with external card IDs or a valid
392 # synced when they contain the no-sync marker.413 # branch are not synced when they contain the no-sync marker.
393 card = Record(title=u'(no-sync)', description=u'(no-sync)',414 card = Record(title=u'(no-sync)', description=u'(no-sync)',
394 external_card_id='11')415 external_card_id=u'11', external_system_url=BRANCH_URL)
395 self.assertFalse(should_sync_card(card, {'autosync': 'on',416 self.assertFalse(should_sync_card(card, {'autosync': 'on',
396 'sync_cards': 'on'}))417 'sync_cards': 'on'}))
397418
@@ -452,7 +473,7 @@
452 self.assertFalse(should_sync_card(card1, conf))473 self.assertFalse(should_sync_card(card1, conf))
453 self.assertFalse(should_sync_card(card2, conf))474 self.assertFalse(should_sync_card(card2, conf))
454475
455 def test_get_card_status_noop(self):476 def test_get_card_status_noop_no_branch(self):
456 # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses,477 # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses,
457 # the status is unknown.478 # the status is unknown.
458 self.assertEqual(None, get_card_status('New', [], None))479 self.assertEqual(None, get_card_status('New', [], None))
@@ -460,6 +481,33 @@
460 self.assertEqual(None, get_card_status('Incomplete', [], None))481 self.assertEqual(None, get_card_status('Incomplete', [], None))
461 self.assertEqual(None, get_card_status('Triaged', [], None))482 self.assertEqual(None, get_card_status('Triaged', [], None))
462483
484 def test_get_card_status_coding_no_bug(self):
485 # For a card with no associated bug, an in progress branch status will
486 # be in the coding state.
487 branch_info = Record(status='In Progress', target=None)
488 self.assertEqual(
489 CardStatus.CODING,
490 get_card_status(None, [], branch_info))
491
492 def test_get_card_status_review_no_bug(self):
493 # For a card with no associated bug, an in review branch status will
494 # be in the review state.
495 branch_info = Record(status='In Review', target=None)
496 self.assertEqual(
497 CardStatus.REVIEW,
498 get_card_status(None, [], branch_info))
499
500 def test_get_card_status_landing_no_bug(self):
501 # For a card with no associated bug, an approved or merged branch
502 # status will be in the landing state.
503 branch_infos = [
504 Record(status='Approved', target=None),
505 Record(status='Merged', target=None)]
506 for branch_info in branch_infos:
507 self.assertEqual(
508 CardStatus.LANDING,
509 get_card_status(None, [], branch_info))
510
463 def test_get_card_status_coding_no_branch(self):511 def test_get_card_status_coding_no_branch(self):
464 # For a bug in 'In Progress' or 'Fix Committed' status, it is512 # For a bug in 'In Progress' or 'Fix Committed' status, it is
465 # considered to be in the coding state if there is no branch.513 # considered to be in the coding state if there is no branch.

Subscribers

People subscribed via source and target branches