Merge lp:~chad.smith/lp2kanban/branch-to-card into lp:~launchpad/lp2kanban/trunk
- branch-to-card
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Данило Шеган (community) | Needs Resubmitting | ||
Landscape | Pending | ||
Review via email: mp+280669@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 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/
# Tweak branches or drag cards at https:/
What's lacking:
unit tests to cover bugs2card sync_board (coming in a followup branch)
Данило Шеган (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
1 | === modified file '.bzrignore' | |||
2 | --- .bzrignore 2012-09-15 18:20:28 +0000 | |||
3 | +++ .bzrignore 2015-12-16 04:22:55 +0000 | |||
4 | @@ -14,3 +14,4 @@ | |||
5 | 14 | .emacs.desktop | 14 | .emacs.desktop |
6 | 15 | credentials | 15 | credentials |
7 | 16 | .emacs.desktop.lock | 16 | .emacs.desktop.lock |
8 | 17 | logs/* | ||
9 | 17 | 18 | ||
10 | === modified file 'Makefile' | |||
11 | --- Makefile 2013-03-18 15:45:56 +0000 | |||
12 | +++ Makefile 2015-12-16 04:22:55 +0000 | |||
13 | @@ -23,12 +23,12 @@ | |||
14 | 23 | 23 | ||
15 | 24 | 24 | ||
16 | 25 | bin/buildout: download-cache | 25 | bin/buildout: download-cache |
18 | 26 | $(PYTHON) bootstrap.py -v 1.7.0 | 26 | $(PYTHON) bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg |
19 | 27 | touch --no-create $@ | 27 | touch --no-create $@ |
20 | 28 | 28 | ||
21 | 29 | 29 | ||
22 | 30 | $(PY): bin/buildout $(BUILDOUT_CFG) setup.py | 30 | $(PY): bin/buildout $(BUILDOUT_CFG) setup.py |
24 | 31 | PYTHONPATH=. ./bin/buildout -c $(BUILDOUT_CFG) | 31 | ./bin/buildout -c $(BUILDOUT_CFG) |
25 | 32 | 32 | ||
26 | 33 | 33 | ||
27 | 34 | $(subst $(PY),,$(BUILDOUT_BIN)): $(PY) | 34 | $(subst $(PY),,$(BUILDOUT_BIN)): $(PY) |
28 | 35 | 35 | ||
29 | === added file 'README.landscape' | |||
30 | --- README.landscape 1970-01-01 00:00:00 +0000 | |||
31 | +++ README.landscape 2015-12-16 04:22:55 +0000 | |||
32 | @@ -0,0 +1,17 @@ | |||
33 | 1 | This branch is based off lp:~bjornt/lp2kanban/landscape and contains the | ||
34 | 2 | configuration files needed for lp2kanban to process bugs for the Landscape | ||
35 | 3 | project. | ||
36 | 4 | |||
37 | 5 | To deploy, first run ./create_creds.py to create the credentials file. You | ||
38 | 6 | should be logged into Launchpad as the lp2kanban user when doing this, unless | ||
39 | 7 | for testing. | ||
40 | 8 | |||
41 | 9 | Next you need to add cron entries to run the bugs2kanban.py script. You will | ||
42 | 10 | need to run it twice, one for creating new bugs (create.ini) and one for | ||
43 | 11 | syncing the card information with bugs (sync.ini): | ||
44 | 12 | |||
45 | 13 | bin/py src/lp2kanban/bugs2kanban.py -c create.ini | ||
46 | 14 | bin/py src/lp2kanban/bugs2kanban.py -c sync.ini | ||
47 | 15 | |||
48 | 16 | The former is quite quick and can be run every minute. The latter is slower | ||
49 | 17 | and shouldn't be run more often than every 5 minutes. | ||
50 | 0 | 18 | ||
51 | === modified file 'bootstrap.py' | |||
52 | --- bootstrap.py 2013-03-18 15:45:56 +0000 | |||
53 | +++ bootstrap.py 2015-12-16 04:22:55 +0000 | |||
54 | @@ -18,7 +18,11 @@ | |||
55 | 18 | use the -c option to specify an alternate configuration file. | 18 | use the -c option to specify an alternate configuration file. |
56 | 19 | """ | 19 | """ |
57 | 20 | 20 | ||
59 | 21 | import os, shutil, sys, tempfile | 21 | import os |
60 | 22 | import shutil | ||
61 | 23 | import sys | ||
62 | 24 | import tempfile | ||
63 | 25 | |||
64 | 22 | from optparse import OptionParser | 26 | from optparse import OptionParser |
65 | 23 | 27 | ||
66 | 24 | tmpeggs = tempfile.mkdtemp() | 28 | tmpeggs = tempfile.mkdtemp() |
67 | @@ -31,8 +35,8 @@ | |||
68 | 31 | Simply run this script in a directory containing a buildout.cfg, using the | 35 | Simply run this script in a directory containing a buildout.cfg, using the |
69 | 32 | Python that you want bin/buildout to use. | 36 | Python that you want bin/buildout to use. |
70 | 33 | 37 | ||
73 | 34 | Note that by using --setup-source and --download-base to point to | 38 | Note that by using --find-links to point to local resources, you can keep |
74 | 35 | local resources, you can keep this script from going over the network. | 39 | this script from going over the network. |
75 | 36 | ''' | 40 | ''' |
76 | 37 | 41 | ||
77 | 38 | parser = OptionParser(usage=usage) | 42 | parser = OptionParser(usage=usage) |
78 | @@ -48,48 +52,63 @@ | |||
79 | 48 | "bootstrap and buildout will get the newest releases " | 52 | "bootstrap and buildout will get the newest releases " |
80 | 49 | "even if they are alphas or betas.")) | 53 | "even if they are alphas or betas.")) |
81 | 50 | parser.add_option("-c", "--config-file", | 54 | parser.add_option("-c", "--config-file", |
84 | 51 | help=("Specify the path to the buildout configuration " | 55 | help=("Specify the path to the buildout configuration " |
85 | 52 | "file to be used.")) | 56 | "file to be used.")) |
86 | 53 | parser.add_option("-f", "--find-links", | 57 | parser.add_option("-f", "--find-links", |
88 | 54 | help=("Specify a URL to search for buildout releases")) | 58 | help=("Specify a URL to search for buildout releases")) |
89 | 59 | parser.add_option("--allow-site-packages", | ||
90 | 60 | action="store_true", default=False, | ||
91 | 61 | help=("Let bootstrap.py use existing site packages")) | ||
92 | 62 | parser.add_option("--setuptools-version", | ||
93 | 63 | help="use a specific setuptools version") | ||
94 | 55 | 64 | ||
95 | 56 | 65 | ||
96 | 57 | options, args = parser.parse_args() | 66 | options, args = parser.parse_args() |
97 | 58 | 67 | ||
98 | 59 | ###################################################################### | 68 | ###################################################################### |
100 | 60 | # load/install distribute | 69 | # load/install setuptools |
101 | 61 | 70 | ||
102 | 62 | to_reload = False | ||
103 | 63 | try: | 71 | try: |
108 | 64 | import pkg_resources, setuptools | 72 | if options.allow_site_packages: |
109 | 65 | if not hasattr(pkg_resources, '_distribute'): | 73 | import setuptools |
110 | 66 | to_reload = True | 74 | import pkg_resources |
111 | 67 | raise ImportError | 75 | from urllib.request import urlopen |
112 | 68 | except ImportError: | 76 | except ImportError: |
132 | 69 | ez = {} | 77 | from urllib2 import urlopen |
133 | 70 | 78 | ||
134 | 71 | try: | 79 | ez = {} |
135 | 72 | from urllib.request import urlopen | 80 | exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) |
136 | 73 | except ImportError: | 81 | |
137 | 74 | from urllib2 import urlopen | 82 | if not options.allow_site_packages: |
138 | 75 | 83 | # ez_setup imports site, which adds site packages | |
139 | 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 |
140 | 77 | setup_args = dict(to_dir=tmpeggs, download_delay=0, no_fake=True) | 85 | # of setuptools are not in the path |
141 | 78 | ez['use_setuptools'](**setup_args) | 86 | import site |
142 | 79 | 87 | # inside a virtualenv, there is no 'getsitepackages'. | |
143 | 80 | if to_reload: | 88 | # We can't remove these reliably |
144 | 81 | reload(pkg_resources) | 89 | if hasattr(site, 'getsitepackages'): |
145 | 82 | import pkg_resources | 90 | for sitepackage_path in site.getsitepackages(): |
146 | 83 | # This does not (always?) update the default working set. We will | 91 | sys.path[:] = [x for x in sys.path if sitepackage_path not in x] |
147 | 84 | # do it. | 92 | |
148 | 85 | for path in sys.path: | 93 | setup_args = dict(to_dir=tmpeggs, download_delay=0) |
149 | 86 | if path not in pkg_resources.working_set.entries: | 94 | |
150 | 87 | pkg_resources.working_set.add_entry(path) | 95 | if options.setuptools_version is not None: |
151 | 96 | setup_args['version'] = options.setuptools_version | ||
152 | 97 | |||
153 | 98 | ez['use_setuptools'](**setup_args) | ||
154 | 99 | import setuptools | ||
155 | 100 | import pkg_resources | ||
156 | 101 | |||
157 | 102 | # This does not (always?) update the default working set. We will | ||
158 | 103 | # do it. | ||
159 | 104 | for path in sys.path: | ||
160 | 105 | if path not in pkg_resources.working_set.entries: | ||
161 | 106 | pkg_resources.working_set.add_entry(path) | ||
162 | 88 | 107 | ||
163 | 89 | ###################################################################### | 108 | ###################################################################### |
164 | 90 | # Install buildout | 109 | # Install buildout |
165 | 91 | 110 | ||
167 | 92 | ws = pkg_resources.working_set | 111 | ws = pkg_resources.working_set |
168 | 93 | 112 | ||
169 | 94 | cmd = [sys.executable, '-c', | 113 | cmd = [sys.executable, '-c', |
170 | 95 | 'from setuptools.command.easy_install import main; main()', | 114 | 'from setuptools.command.easy_install import main; main()', |
171 | @@ -104,8 +123,8 @@ | |||
172 | 104 | if find_links: | 123 | if find_links: |
173 | 105 | cmd.extend(['-f', find_links]) | 124 | cmd.extend(['-f', find_links]) |
174 | 106 | 125 | ||
177 | 107 | distribute_path = ws.find( | 126 | setuptools_path = ws.find( |
178 | 108 | pkg_resources.Requirement.parse('distribute')).location | 127 | pkg_resources.Requirement.parse('setuptools')).location |
179 | 109 | 128 | ||
180 | 110 | requirement = 'zc.buildout' | 129 | requirement = 'zc.buildout' |
181 | 111 | version = options.version | 130 | version = options.version |
182 | @@ -113,13 +132,19 @@ | |||
183 | 113 | # Figure out the most recent final version of zc.buildout. | 132 | # Figure out the most recent final version of zc.buildout. |
184 | 114 | import setuptools.package_index | 133 | import setuptools.package_index |
185 | 115 | _final_parts = '*final-', '*final' | 134 | _final_parts = '*final-', '*final' |
186 | 135 | |||
187 | 116 | def _final_version(parsed_version): | 136 | def _final_version(parsed_version): |
192 | 117 | for part in parsed_version: | 137 | try: |
193 | 118 | if (part[:1] == '*') and (part not in _final_parts): | 138 | return not parsed_version.is_prerelease |
194 | 119 | return False | 139 | except AttributeError: |
195 | 120 | return True | 140 | # Older setuptools |
196 | 141 | for part in parsed_version: | ||
197 | 142 | if (part[:1] == '*') and (part not in _final_parts): | ||
198 | 143 | return False | ||
199 | 144 | return True | ||
200 | 145 | |||
201 | 121 | index = setuptools.package_index.PackageIndex( | 146 | index = setuptools.package_index.PackageIndex( |
203 | 122 | search_path=[distribute_path]) | 147 | search_path=[setuptools_path]) |
204 | 123 | if find_links: | 148 | if find_links: |
205 | 124 | index.add_find_links((find_links,)) | 149 | index.add_find_links((find_links,)) |
206 | 125 | req = pkg_resources.Requirement.parse(requirement) | 150 | req = pkg_resources.Requirement.parse(requirement) |
207 | @@ -142,10 +167,9 @@ | |||
208 | 142 | cmd.append(requirement) | 167 | cmd.append(requirement) |
209 | 143 | 168 | ||
210 | 144 | import subprocess | 169 | import subprocess |
212 | 145 | if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=distribute_path)) != 0: | 170 | if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: |
213 | 146 | raise Exception( | 171 | raise Exception( |
216 | 147 | "Failed to execute command:\n%s", | 172 | "Failed to execute command:\n%s" % repr(cmd)[1:-1]) |
215 | 148 | repr(cmd)[1:-1]) | ||
217 | 149 | 173 | ||
218 | 150 | ###################################################################### | 174 | ###################################################################### |
219 | 151 | # Import and run buildout | 175 | # Import and run buildout |
220 | 152 | 176 | ||
221 | === modified file 'buildout.cfg' | |||
222 | --- buildout.cfg 2013-06-19 19:44:20 +0000 | |||
223 | +++ buildout.cfg 2015-12-16 04:22:55 +0000 | |||
224 | @@ -11,6 +11,7 @@ | |||
225 | 11 | exec-sitecustomize = true | 11 | exec-sitecustomize = true |
226 | 12 | develop = . | 12 | develop = . |
227 | 13 | eggs-directory = eggs | 13 | eggs-directory = eggs |
228 | 14 | offline = false | ||
229 | 14 | 15 | ||
230 | 15 | [test] | 16 | [test] |
231 | 16 | recipe = zc.recipe.testrunner | 17 | recipe = zc.recipe.testrunner |
232 | @@ -38,13 +39,12 @@ | |||
233 | 38 | lazr.restfulclient = 0.11.2 | 39 | lazr.restfulclient = 0.11.2 |
234 | 39 | lazr.uri = 1.0.2 | 40 | lazr.uri = 1.0.2 |
235 | 40 | oauth = 1.0.1 | 41 | oauth = 1.0.1 |
236 | 41 | setuptools = 0.6c11 | ||
237 | 42 | simplejson = 2.1.6 | 42 | simplejson = 2.1.6 |
238 | 43 | wadllib = 1.2.0 | 43 | wadllib = 1.2.0 |
239 | 44 | wsgi-intercept = 0.5.0 | 44 | wsgi-intercept = 0.5.0 |
240 | 45 | z3c.recipe.scripts = 1.0.1 | 45 | z3c.recipe.scripts = 1.0.1 |
241 | 46 | z3c.recipe.tag = 0.4.0 | 46 | z3c.recipe.tag = 0.4.0 |
243 | 47 | zc.buildout = 1.5.2 | 47 | zc.buildout = 1.7.1 |
244 | 48 | zc.recipe.egg = 1.3.2 | 48 | zc.recipe.egg = 1.3.2 |
245 | 49 | zc.recipe.testrunner = 1.4.0 | 49 | zc.recipe.testrunner = 1.4.0 |
246 | 50 | zope.exceptions = 3.6.1 | 50 | zope.exceptions = 3.6.1 |
247 | 51 | 51 | ||
248 | === added directory 'configs' | |||
249 | === added file 'configs/groups.ini' | |||
250 | --- configs/groups.ini 1970-01-01 00:00:00 +0000 | |||
251 | +++ configs/groups.ini 2015-12-16 04:22:55 +0000 | |||
252 | @@ -0,0 +1,16 @@ | |||
253 | 1 | # The group name, which will replace ${group} in lane configs. | ||
254 | 2 | # The Launchpad account names of the group members. | ||
255 | 3 | [Alpha] | ||
256 | 4 | members = benji,simpoir | ||
257 | 5 | [Beta] | ||
258 | 6 | members = fcorrea,chad.smith | ||
259 | 7 | [Gamma] | ||
260 | 8 | members = adam-collard,bogdana | ||
261 | 9 | [Delta] | ||
262 | 10 | members = tealeg,danilo | ||
263 | 11 | [Epsilon] | ||
264 | 12 | members = free.ekanayaka,ack | ||
265 | 13 | [Zeta] | ||
266 | 14 | members = bjornt,tribaal | ||
267 | 15 | [QA] | ||
268 | 16 | members = ahasenack | ||
269 | 0 | 17 | ||
270 | === added file 'configs/sync.ini' | |||
271 | --- configs/sync.ini 1970-01-01 00:00:00 +0000 | |||
272 | +++ configs/sync.ini 2015-12-16 04:22:55 +0000 | |||
273 | @@ -0,0 +1,106 @@ | |||
274 | 1 | [GLOBAL] | ||
275 | 2 | # This is the account part of https://<account>.leankitkanban.com/ URL. | ||
276 | 3 | account = canonical | ||
277 | 4 | # LeankitKanban user name and password. | ||
278 | 5 | user = landscape-crew@lists.canonical.com | ||
279 | 6 | password = fShv0K4h@LV:!{ | ||
280 | 7 | |||
281 | 8 | # Defaults are common values overridable in each of the boards definition. | ||
282 | 9 | [DEFAULT] | ||
283 | 10 | lp_server = production | ||
284 | 11 | launchpadlib_dir = .launchpadlib.create | ||
285 | 12 | |||
286 | 13 | # Should all cards with external IDs be synced or not. Set to 'on' if | ||
287 | 14 | # you want them synced automatically (and use "(no-sync)" as the marker | ||
288 | 15 | # for cards you don't want synced). | ||
289 | 16 | autosync = on | ||
290 | 17 | |||
291 | 18 | # Should the cards be moved? If not specified it is off. | ||
292 | 19 | move_cards = on | ||
293 | 20 | |||
294 | 21 | # Should the cards be synced? If not specified it is off. | ||
295 | 22 | sync_cards = on | ||
296 | 23 | |||
297 | 24 | # Bug tag for which new cards should be created. If a bug in Launchpad | ||
298 | 25 | # has this tag, a new card will be created for it in the first kanban lane. | ||
299 | 26 | # If this config option isn't set, no cards will be created. | ||
300 | 27 | # After the card has been created, the tag will be removed from the bug, so a | ||
301 | 28 | # credentials_file has to be specified as well, to authenticate against LP. | ||
302 | 29 | bug_to_card_tag = kanban | ||
303 | 30 | |||
304 | 31 | # Which card type should cards created from bugs have. If not set, the default | ||
305 | 32 | # card type is used. | ||
306 | 33 | bug_to_card_type = Bug | ||
307 | 34 | |||
308 | 35 | bug_to_card_lane = Backlog | ||
309 | 36 | |||
310 | 37 | # By default, omnidirectional card moves are off, which means that cards will | ||
311 | 38 | # be moved only to the lane that is considered to be next of the card's | ||
312 | 39 | # current lane. I.e. a card will move only within a "swim lane". If | ||
313 | 40 | # omnidirectional card moves are turned on, a card can move to any lane | ||
314 | 41 | # on the board. | ||
315 | 42 | omnidirectional_card_moves = on | ||
316 | 43 | |||
317 | 44 | # Lanes are assigned a "role" by looking for sublanes matching | ||
318 | 45 | # values given here. "Coding" will match all lanes which have | ||
319 | 46 | # a title of "Coding", and "QA::Ready" will match all lanes | ||
320 | 47 | # titled "Ready" that have a parent lane with title "QA". | ||
321 | 48 | # | ||
322 | 49 | # Note that bugs2cards.py analyzes the lane structure and will flow | ||
323 | 50 | # cards only within the same horizontal "lane" (iow, if you have | ||
324 | 51 | # parallel 'Feature' and 'Bugs' development lanes, cards will flow | ||
325 | 52 | # only from Coding -> Review inside them, and then will move to the | ||
326 | 53 | # next lane horizontally (eg. Landing lane). | ||
327 | 54 | #coding_lanes = | ||
328 | 55 | #review_lanes = | ||
329 | 56 | #landing_lanes = | ||
330 | 57 | #qa_lanes = | ||
331 | 58 | #deploy_lanes = | ||
332 | 59 | #downtime_deploy_lanes = | ||
333 | 60 | #done_lanes = | ||
334 | 61 | |||
335 | 62 | |||
336 | 63 | # If you track multiple groups of people on one board, you can specify a | ||
337 | 64 | # groups config that will map a card to a group according to its | ||
338 | 65 | #assignee. You can then make the lane configs to include the group | ||
339 | 66 | # name, by using ${group} in the lane name. For example: | ||
340 | 67 | # | ||
341 | 68 | # coding_lanes = ${group}::Coding | ||
342 | 69 | # | ||
343 | 70 | groups_config_file = ./configs/groups.ini | ||
344 | 71 | |||
345 | 72 | credentials_file = ./credentials | ||
346 | 73 | |||
347 | 74 | |||
348 | 75 | #[Landscape Cisco] | ||
349 | 76 | # Keep [Landscape Cisco] board before [Landscape] so it gets the "falkor" cards | ||
350 | 77 | # (we can only use external ID for one card across all of Leankit). | ||
351 | 78 | #projects = falkor | ||
352 | 79 | |||
353 | 80 | #bug_to_card_lane = Backlog | ||
354 | 81 | |||
355 | 82 | #coding_lanes = Doing | ||
356 | 83 | #review_lanes = Doing | ||
357 | 84 | #landing_lanes = Done | ||
358 | 85 | #deploy_lanes = Done | ||
359 | 86 | #done_lanes = Done | ||
360 | 87 | |||
361 | 88 | #no_move_lanes = | ||
362 | 89 | #no_sync_lanes = | ||
363 | 90 | |||
364 | 91 | |||
365 | 92 | [Landscape 2016] | ||
366 | 93 | project_group = landscape-project | ||
367 | 94 | |||
368 | 95 | # TODO: This should put charm bugs in ::Charm and server/client bugs | ||
369 | 96 | # in ::Landscape, for now stick everything in ::Landscape | ||
370 | 97 | bug_to_card_lane = Backlog::Engineering | ||
371 | 98 | |||
372 | 99 | coding_lanes = Development::${group}::Doing | ||
373 | 100 | review_lanes = Development::${group}::Review | ||
374 | 101 | landing_lanes = Development::${group}::Landed | ||
375 | 102 | deploy_lanes = QA::Landed | ||
376 | 103 | done_lanes = Done | ||
377 | 104 | |||
378 | 105 | no_move_lanes = Done,QA::Landed,QA::In progress | ||
379 | 106 | no_sync_lanes = Backlog::Engineering,Done,QA::Landed,QA::In progress,Archive,Ready for QA,Release Ready,Staging,Production,LDS | ||
380 | 0 | 107 | ||
381 | === modified file 'create_creds.py' | |||
382 | --- create_creds.py 2012-12-10 15:21:44 +0000 | |||
383 | +++ create_creds.py 2015-12-16 04:22:55 +0000 | |||
384 | @@ -7,7 +7,7 @@ | |||
385 | 7 | 7 | ||
386 | 8 | web_root="https://launchpad.net/" | 8 | web_root="https://launchpad.net/" |
387 | 9 | 9 | ||
389 | 10 | creds = Credentials("tarmac") | 10 | creds = Credentials("lp2kanban-landscape") |
390 | 11 | url = creds.get_request_token(web_root=web_root) | 11 | url = creds.get_request_token(web_root=web_root) |
391 | 12 | 12 | ||
392 | 13 | subprocess.call(['xdg-open', url]) | 13 | subprocess.call(['xdg-open', url]) |
393 | 14 | 14 | ||
394 | === added file 'jenkins.sh' | |||
395 | --- jenkins.sh 1970-01-01 00:00:00 +0000 | |||
396 | +++ jenkins.sh 2015-12-16 04:22:55 +0000 | |||
397 | @@ -0,0 +1,32 @@ | |||
398 | 1 | #!/bin/bash -xe | ||
399 | 2 | |||
400 | 3 | ini="$1.ini" | ||
401 | 4 | date "+START: %c" | ||
402 | 5 | |||
403 | 6 | #export https_proxy=http://squid.external:3128 | ||
404 | 7 | #export http_proxy=http://squid.external:3128 | ||
405 | 8 | |||
406 | 9 | make | ||
407 | 10 | |||
408 | 11 | if [ ! -e credentials ]; then | ||
409 | 12 | if [ -e ~/credentials ]; then | ||
410 | 13 | cp ~/credentials credentials | ||
411 | 14 | else | ||
412 | 15 | echo "ERROR: try make credentials first!" | ||
413 | 16 | exit 1 | ||
414 | 17 | fi | ||
415 | 18 | fi | ||
416 | 19 | |||
417 | 20 | # default encoding of stdout/stderr when not attached to a terminal is set to | ||
418 | 21 | # 'None'. 'PYTHONIOENCODING' overrides, could also be overridden in the | ||
419 | 22 | # python program by modifying 'sys.stdout.encoding' directly. This feels | ||
420 | 23 | # cleaner. Note setting LANG (which the terminal respects) wont fix this, | ||
421 | 24 | # since there is no terminal attached. | ||
422 | 25 | # -------- | ||
423 | 26 | # See: | ||
424 | 27 | # - https://wiki.python.org/moin/PrintFails | ||
425 | 28 | # - https://stackoverflow.com/questions/1473577 | ||
426 | 29 | # - https://stackoverflow.com/questions/492483 | ||
427 | 30 | # - https://stackoverflow.com/questions/9932406 | ||
428 | 31 | PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini | ||
429 | 32 | date "+END: %c" | ||
430 | 0 | 33 | ||
431 | === added directory 'logs' | |||
432 | === added file 'run.sh' | |||
433 | --- run.sh 1970-01-01 00:00:00 +0000 | |||
434 | +++ run.sh 2015-12-16 04:22:55 +0000 | |||
435 | @@ -0,0 +1,21 @@ | |||
436 | 1 | #!/bin/bash | ||
437 | 2 | |||
438 | 3 | LOGFILE="logs/${1}-`date '+%Y-%m-%d'`.log" | ||
439 | 4 | LOCKFILE="$HOME/lp2kanban/run-$1.lock" | ||
440 | 5 | exit_status=0 | ||
441 | 6 | export https_proxy=http://squid.external:3128 | ||
442 | 7 | |||
443 | 8 | cd $HOME/lp2kanban | ||
444 | 9 | echo -n "START " >> $LOGFILE | ||
445 | 10 | date "+%c" >> $LOGFILE | ||
446 | 11 | lockfile -10 -r 4 $LOCKFILE | ||
447 | 12 | if [ "$?" -eq 0 ]; then | ||
448 | 13 | bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini 2>&1 >> $LOGFILE | ||
449 | 14 | exit_status=$? | ||
450 | 15 | rm -f $LOCKFILE | ||
451 | 16 | else | ||
452 | 17 | echo "Failed to grab lockfile $LOCKFILE" >> $LOGFILE | ||
453 | 18 | exit_status=1 | ||
454 | 19 | fi | ||
455 | 20 | echo -n "END " >> $LOGFILE; date "+%c" >> $LOGFILE | ||
456 | 21 | exit $? | ||
457 | 0 | 22 | ||
458 | === modified file 'src/lp2kanban/bugs2cards.py' | |||
459 | --- src/lp2kanban/bugs2cards.py 2013-06-19 19:44:20 +0000 | |||
460 | +++ src/lp2kanban/bugs2cards.py 2015-12-16 04:22:55 +0000 | |||
461 | @@ -1,4 +1,4 @@ | |||
463 | 1 | # Copyright 2011 Canonical Ltd | 1 | # Copyright 2015 Canonical Ltd |
464 | 2 | # | 2 | # |
465 | 3 | from ConfigParser import ConfigParser | 3 | from ConfigParser import ConfigParser |
466 | 4 | from argparse import ArgumentParser | 4 | from argparse import ArgumentParser |
467 | @@ -9,6 +9,7 @@ | |||
468 | 9 | update_blueprints_from_work_items, | 9 | update_blueprints_from_work_items, |
469 | 10 | ) | 10 | ) |
470 | 11 | from lp2kanban.kanban import ( | 11 | from lp2kanban.kanban import ( |
471 | 12 | BRANCH_REGEX, | ||
472 | 12 | LeankitKanban, | 13 | LeankitKanban, |
473 | 13 | Record, | 14 | Record, |
474 | 14 | ) | 15 | ) |
475 | @@ -117,6 +118,9 @@ | |||
476 | 117 | for user in board.users: | 118 | for user in board.users: |
477 | 118 | lp_user = lp.people.getByEmail(email=user) | 119 | lp_user = lp.people.getByEmail(email=user) |
478 | 119 | if lp_user is None: | 120 | if lp_user is None: |
479 | 121 | print ( | ||
480 | 122 | "WARNING: There is no matching Launchpad user for kanban" | ||
481 | 123 | " user %s." % (user,)) | ||
482 | 120 | continue | 124 | continue |
483 | 121 | self.lp_to_kanban[lp_user.name] = board.users[user] | 125 | self.lp_to_kanban[lp_user.name] = board.users[user] |
484 | 122 | self.kanban_to_lp[board.users[user]] = lp_user | 126 | self.kanban_to_lp[board.users[user]] = lp_user |
485 | @@ -161,8 +165,10 @@ | |||
486 | 161 | 165 | ||
487 | 162 | def _get_mp_info(): | 166 | def _get_mp_info(): |
488 | 163 | mps = [] | 167 | mps = [] |
491 | 164 | for bug_branch in branches: | 168 | for branch in branches: |
492 | 165 | for mp in bug_branch.branch.landing_targets: | 169 | if hasattr(branch, 'branch') : |
493 | 170 | branch = branch.branch | ||
494 | 171 | for mp in branch.landing_targets: | ||
495 | 166 | mp_info = Record(rank=None, status=None, mp=None) | 172 | mp_info = Record(rank=None, status=None, mp=None) |
496 | 167 | status = mp.queue_status | 173 | status = mp.queue_status |
497 | 168 | mp_info.rank = ORDERED_STATUSES.get(status, 1) | 174 | mp_info.rank = ORDERED_STATUSES.get(status, 1) |
498 | @@ -222,8 +228,10 @@ | |||
499 | 222 | if conf.get('autosync', None) == 'on': | 228 | if conf.get('autosync', None) == 'on': |
500 | 223 | has_id = (card.external_card_id is not None and | 229 | has_id = (card.external_card_id is not None and |
501 | 224 | card.external_card_id.strip() != '') | 230 | card.external_card_id.strip() != '') |
504 | 225 | return (has_id and NO_SYNC_MARKER not in card.title and | 231 | has_branch = BRANCH_REGEX.match(card.external_system_url) |
505 | 226 | NO_SYNC_MARKER not in card.description) | 232 | if NO_SYNC_MARKER in card.title or NO_SYNC_MARKER in card.description: |
506 | 233 | return False | ||
507 | 234 | return (has_id or has_branch) | ||
508 | 227 | else: | 235 | else: |
509 | 228 | return (card.title.startswith(TITLE_MARKER) or | 236 | return (card.title.startswith(TITLE_MARKER) or |
510 | 229 | DESCRIPTION_MARKER in card.description) | 237 | DESCRIPTION_MARKER in card.description) |
511 | @@ -296,15 +304,21 @@ | |||
512 | 296 | """Return the status of the card as one of CardStatus values.""" | 304 | """Return the status of the card as one of CardStatus values.""" |
513 | 297 | # status of None means card needs not to be moved. | 305 | # status of None means card needs not to be moved. |
514 | 298 | status = None | 306 | status = None |
517 | 299 | if bug_status in IN_PROGRESS_BUG_STATUSES: | 307 | branch_card_status = None |
518 | 300 | status = CardStatus.CODING | 308 | if branch_info: |
519 | 301 | if branch_info.status == 'In Progress': | 309 | if branch_info.status == 'In Progress': |
521 | 302 | status = CardStatus.CODING | 310 | branch_card_status = CardStatus.CODING |
522 | 303 | elif branch_info.status == 'In Review': | 311 | elif branch_info.status == 'In Review': |
524 | 304 | status = CardStatus.REVIEW | 312 | branch_card_status = CardStatus.REVIEW |
525 | 305 | elif branch_info.status == 'Approved': | 313 | elif branch_info.status == 'Approved': |
527 | 306 | status = CardStatus.LANDING | 314 | branch_card_status = CardStatus.LANDING |
528 | 307 | elif branch_info.status == 'Merged': | 315 | elif branch_info.status == 'Merged': |
529 | 316 | branch_card_status = CardStatus.LANDING | ||
530 | 317 | if bug_status is None: | ||
531 | 318 | return branch_card_status | ||
532 | 319 | if bug_status in IN_PROGRESS_BUG_STATUSES: | ||
533 | 320 | status = CardStatus.CODING | ||
534 | 321 | if branch_info.status == 'Merged': | ||
535 | 308 | if bug_status == 'Fix Committed': | 322 | if bug_status == 'Fix Committed': |
536 | 309 | if 'qa-needstesting' in bug_tags: | 323 | if 'qa-needstesting' in bug_tags: |
537 | 310 | status = CardStatus.QA | 324 | status = CardStatus.QA |
538 | @@ -315,6 +329,8 @@ | |||
539 | 315 | status = CardStatus.DEPLOY | 329 | status = CardStatus.DEPLOY |
540 | 316 | else: | 330 | else: |
541 | 317 | status = CardStatus.LANDING | 331 | status = CardStatus.LANDING |
542 | 332 | elif branch_card_status: | ||
543 | 333 | return branch_card_status | ||
544 | 318 | elif bug_status in DONE_BUG_STATUSES: | 334 | elif bug_status in DONE_BUG_STATUSES: |
545 | 319 | status = CardStatus.DONE | 335 | status = CardStatus.DONE |
546 | 320 | return status | 336 | return status |
547 | @@ -454,6 +470,37 @@ | |||
548 | 454 | board, project, new_bug_tag, bconf.get("bug_to_card_type"), | 470 | board, project, new_bug_tag, bconf.get("bug_to_card_type"), |
549 | 455 | bconf.get("bug_to_card_lane")) | 471 | bconf.get("bug_to_card_lane")) |
550 | 456 | print " Syncing cards:" | 472 | print " Syncing cards:" |
551 | 473 | # Process linked branch cards | ||
552 | 474 | for card in board.getCardsWithExternalLinks(only_branches=True): | ||
553 | 475 | if card.external_card_id: | ||
554 | 476 | # Cards with external_id are processed by getCardsWithExternalIds | ||
555 | 477 | continue | ||
556 | 478 | branch_name = card.external_system_url.replace( | ||
557 | 479 | "https://code.launchpad.net/", "") | ||
558 | 480 | branch = lp.branches.getByUniqueName(unique_name=branch_name) | ||
559 | 481 | if not branch: | ||
560 | 482 | print "Invalid branch url ({}) for card '{}'".format( | ||
561 | 483 | card.external_system_url, card.title) | ||
562 | 484 | continue | ||
563 | 485 | if should_sync_card(card, bconf): | ||
564 | 486 | branch_info = get_branch_info([branch]) | ||
565 | 487 | owner_name = BRANCH_REGEX.match(card.external_system_url).group(1) | ||
566 | 488 | card_status = get_card_status(None, '', branch_info) | ||
567 | 489 | if branch_info.status and should_move_card(card, bconf): | ||
568 | 490 | assignee = Record(name=owner_name) | ||
569 | 491 | try: | ||
570 | 492 | move_card(card, card_status, bconf, [assignee], lp_users) | ||
571 | 493 | except IOError as e: | ||
572 | 494 | print " * %s (in %s)" % (card.title, card.lane.path) | ||
573 | 495 | print " >>> Error moving card:" | ||
574 | 496 | print " ", e | ||
575 | 497 | continue | ||
576 | 498 | kanban_user = lp_users.lp_to_kanban.get(assignee.name, None) | ||
577 | 499 | if kanban_user: | ||
578 | 500 | card.assigned_user_id = kanban_user.id | ||
579 | 501 | card.kanban_user = kanban_user | ||
580 | 502 | print " * %s (in %s)" % (card.title, card.lane.path) | ||
581 | 503 | |||
582 | 457 | for card in board.getCardsWithExternalIds(): | 504 | for card in board.getCardsWithExternalIds(): |
583 | 458 | if should_sync_card(card, bconf): | 505 | if should_sync_card(card, bconf): |
584 | 459 | try: | 506 | try: |
585 | 460 | 507 | ||
586 | === modified file 'src/lp2kanban/kanban.py' | |||
587 | --- src/lp2kanban/kanban.py 2013-05-24 14:48:07 +0000 | |||
588 | +++ src/lp2kanban/kanban.py 2015-12-16 04:22:55 +0000 | |||
589 | @@ -11,6 +11,8 @@ | |||
590 | 11 | 11 | ||
591 | 12 | 12 | ||
592 | 13 | ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL) | 13 | ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL) |
593 | 14 | BRANCH_REGEX = re.compile( | ||
594 | 15 | '^https://code.launchpad.net/~([.\w]*)/([-\w]*)/([-\w]*)$') | ||
595 | 14 | 16 | ||
596 | 15 | 17 | ||
597 | 16 | class Record(dict): | 18 | class Record(dict): |
598 | @@ -55,8 +57,7 @@ | |||
599 | 55 | 57 | ||
600 | 56 | class LeankitConnector(object): | 58 | class LeankitConnector(object): |
601 | 57 | def __init__(self, account, username=None, password=None, throttle=1): | 59 | def __init__(self, account, username=None, password=None, throttle=1): |
604 | 58 | host = 'https://' + account + '.leankitkanban.com' | 60 | self.base_api_url = 'https://' + account + '.leankit.com' |
603 | 59 | self.base_api_url = host + '/Kanban/Api' | ||
605 | 60 | self.http = self._configure_auth(username, password) | 61 | self.http = self._configure_auth(username, password) |
606 | 61 | self.last_request_time = time.time() - throttle | 62 | self.last_request_time = time.time() - throttle |
607 | 62 | self.throttle = throttle | 63 | self.throttle = throttle |
608 | @@ -97,6 +98,10 @@ | |||
609 | 97 | config={}, | 98 | config={}, |
610 | 98 | return_response=False) | 99 | return_response=False) |
611 | 99 | sent = request.send() | 100 | sent = request.send() |
612 | 101 | except AttributeError as e: | ||
613 | 102 | # Weirdly, httplib2 has a habit of throwing an AttributeError | ||
614 | 103 | # when it can't connect, so we handle that nicely. | ||
615 | 104 | raise IOError("Unable to connect to LeanKitKanban server.") | ||
616 | 100 | except Exception as e: | 105 | except Exception as e: |
617 | 101 | raise IOError("Unable to make HTTP request: %s" % e.message) | 106 | raise IOError("Unable to make HTTP request: %s" % e.message) |
618 | 102 | 107 | ||
619 | @@ -193,14 +198,17 @@ | |||
620 | 193 | optional_attributes = [ | 198 | optional_attributes = [ |
621 | 194 | 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked', | 199 | 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked', |
622 | 195 | 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl', | 200 | 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl', |
624 | 196 | 'ClassOfServiceId', 'DueDate', | 201 | 'ClassOfServiceId', 'DueDate', 'CurrentTaskBoardId' |
625 | 197 | ] | 202 | ] |
626 | 198 | 203 | ||
627 | 199 | def __init__(self, card_dict, lane): | 204 | def __init__(self, card_dict, lane): |
628 | 200 | super(LeankitCard, self).__init__(card_dict) | 205 | super(LeankitCard, self).__init__(card_dict) |
629 | 201 | 206 | ||
630 | 202 | self.lane = lane | 207 | self.lane = lane |
632 | 203 | self.tags_list = set([tag.strip() for tag in self.tags.split(',')]) | 208 | if not self.tags: |
633 | 209 | self.tags_list = [] | ||
634 | 210 | else: | ||
635 | 211 | self.tags_list = set([tag.strip() for tag in self.tags.split(',')]) | ||
636 | 204 | if '' in self.tags_list: | 212 | if '' in self.tags_list: |
637 | 205 | self.tags_list.remove('') | 213 | self.tags_list.remove('') |
638 | 206 | self.type = lane.board.cardtypes[self.type_id] | 214 | self.type = lane.board.cardtypes[self.type_id] |
639 | @@ -214,7 +222,7 @@ | |||
640 | 214 | tag = tag.strip() | 222 | tag = tag.strip() |
641 | 215 | if tag not in self.tags_list or self.tags.startswith(','): | 223 | if tag not in self.tags_list or self.tags.startswith(','): |
642 | 216 | if tag != '': | 224 | if tag != '': |
644 | 217 | self.tags_list.add(tag) | 225 | self.tags_list.append(tag) |
645 | 218 | self.tags = ', '.join(self.tags_list) | 226 | self.tags = ', '.join(self.tags_list) |
646 | 219 | 227 | ||
647 | 220 | def save(self): | 228 | def save(self): |
648 | @@ -241,10 +249,12 @@ | |||
649 | 241 | del data['Id'] | 249 | del data['Id'] |
650 | 242 | del data['LaneId'] | 250 | del data['LaneId'] |
651 | 243 | position = len(self.lane.cards) | 251 | position = len(self.lane.cards) |
654 | 244 | url_parts = ['/Board', str(self.lane.board.id), 'AddCard', | 252 | url_parts = ['/Kanban/Api/Board', str(self.lane.board.id), |
655 | 245 | 'Lane', str(self.lane.id), 'Position', str(position)] | 253 | 'AddCard', 'Lane', str(self.lane.id), |
656 | 254 | 'Position', str(position)] | ||
657 | 246 | else: | 255 | else: |
659 | 247 | url_parts = ['/Board', str(self.lane.board.id), 'UpdateCard'] | 256 | url_parts = ['/Kanban/Api/Board', str(self.lane.board.id), |
660 | 257 | 'UpdateCard'] | ||
661 | 248 | 258 | ||
662 | 249 | url = '/'.join(url_parts) | 259 | url = '/'.join(url_parts) |
663 | 250 | 260 | ||
664 | @@ -267,9 +277,23 @@ | |||
665 | 267 | else: | 277 | else: |
666 | 268 | return None | 278 | return None |
667 | 269 | 279 | ||
668 | 280 | def moveToTaskBoard(self, parent_board, target_card): | ||
669 | 281 | url = '/Api/Card/MoveCardToTaskboard' | ||
670 | 282 | result = self.lane.board.connector.post(url, data={ | ||
671 | 283 | 'boardId': parent_board.id, | ||
672 | 284 | 'destCardId': target_card.id, | ||
673 | 285 | 'srcCardId': [self.id]}) | ||
674 | 286 | if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES: | ||
675 | 287 | return result.ReplyData[0] | ||
676 | 288 | else: | ||
677 | 289 | raise Exception( | ||
678 | 290 | "Moving card %s (%s) to %s failed. " % ( | ||
679 | 291 | self.title, self.id, self.lane.path) + | ||
680 | 292 | "Error %s: %s" % (result.ReplyCode, result.ReplyText)) | ||
681 | 293 | |||
682 | 270 | def _moveCard(self): | 294 | def _moveCard(self): |
683 | 271 | target_pos = len(self.lane.cards) | 295 | target_pos = len(self.lane.cards) |
685 | 272 | url = '/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % ( | 296 | url = '/Kanban/Api/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % ( |
686 | 273 | self.lane.board.id, self.id, self.lane.id, target_pos) | 297 | self.lane.board.id, self.id, self.lane.id, target_pos) |
687 | 274 | result = self.lane.board.connector.post(url, data=None) | 298 | result = self.lane.board.connector.post(url, data=None) |
688 | 275 | if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES: | 299 | if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES: |
689 | @@ -323,6 +347,8 @@ | |||
690 | 323 | text_after_json), where json_annotations contains the | 347 | text_after_json), where json_annotations contains the |
691 | 324 | JSON loaded with json.loads(). | 348 | JSON loaded with json.loads(). |
692 | 325 | """ | 349 | """ |
693 | 350 | if not self.description: | ||
694 | 351 | self.description = "" | ||
695 | 326 | match = ANNOTATION_REGEX.search(self.description) | 352 | match = ANNOTATION_REGEX.search(self.description) |
696 | 327 | if match: | 353 | if match: |
697 | 328 | start = match.start() | 354 | start = match.start() |
698 | @@ -330,7 +356,7 @@ | |||
699 | 330 | try: | 356 | try: |
700 | 331 | annotations = Record(json.loads(self.description[start:end])) | 357 | annotations = Record(json.loads(self.description[start:end])) |
701 | 332 | except ValueError, ex: | 358 | except ValueError, ex: |
703 | 333 | print "Unable to parse card %i: %s" % (self.id, ex.message) | 359 | print "Unable to parse card %s: %s" % (self.id, ex.message) |
704 | 334 | annotations = Record() | 360 | annotations = Record() |
705 | 335 | return ( | 361 | return ( |
706 | 336 | annotations, | 362 | annotations, |
707 | @@ -450,7 +476,7 @@ | |||
708 | 450 | 476 | ||
709 | 451 | attributes = ['Id', 'Title', 'CreationDate', 'IsArchived'] | 477 | attributes = ['Id', 'Title', 'CreationDate', 'IsArchived'] |
710 | 452 | 478 | ||
712 | 453 | base_uri = '/Boards/' | 479 | base_uri = '/Kanban/Api/Boards/' |
713 | 454 | 480 | ||
714 | 455 | def __init__(self, board_dict, connector): | 481 | def __init__(self, board_dict, connector): |
715 | 456 | super(LeankitBoard, self).__init__(board_dict) | 482 | super(LeankitBoard, self).__init__(board_dict) |
716 | @@ -469,12 +495,20 @@ | |||
717 | 469 | self._cards_with_external_ids = set() | 495 | self._cards_with_external_ids = set() |
718 | 470 | self._cards_with_description_annotations = set() | 496 | self._cards_with_description_annotations = set() |
719 | 471 | self._cards_with_external_links = set() | 497 | self._cards_with_external_links = set() |
720 | 498 | self._cards_with_branches = set() | ||
721 | 472 | self.default_cardtype = None | 499 | self.default_cardtype = None |
722 | 473 | 500 | ||
723 | 474 | def getCardsWithExternalIds(self): | 501 | def getCardsWithExternalIds(self): |
724 | 475 | return self._cards_with_external_ids | 502 | return self._cards_with_external_ids |
725 | 476 | 503 | ||
727 | 477 | def getCardsWithExternalLinks(self): | 504 | def getCardsWithExternalLinks(self, only_branches=False): |
728 | 505 | """Return cards with external links | ||
729 | 506 | |||
730 | 507 | @param only_merge_proposals: Only return cards that have merge | ||
731 | 508 | proposals specified as the card's external link | ||
732 | 509 | """ | ||
733 | 510 | if only_branches: | ||
734 | 511 | return self._cards_with_branches | ||
735 | 478 | return self._cards_with_external_links | 512 | return self._cards_with_external_links |
736 | 479 | 513 | ||
737 | 480 | def getCardsWithDescriptionAnnotations(self): | 514 | def getCardsWithDescriptionAnnotations(self): |
738 | @@ -487,15 +521,23 @@ | |||
739 | 487 | self._populateUsers(self.details['BoardUsers']) | 521 | self._populateUsers(self.details['BoardUsers']) |
740 | 488 | self._populateCardTypes(self.details['CardTypes']) | 522 | self._populateCardTypes(self.details['CardTypes']) |
741 | 489 | self._archive = self.connector.get( | 523 | self._archive = self.connector.get( |
743 | 490 | "/Board/" + str(self.id) + "/Archive").ReplyData[0] | 524 | "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0] |
744 | 491 | archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive] | 525 | archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive] |
745 | 492 | archive_lanes.extend( | 526 | archive_lanes.extend( |
746 | 493 | [lane_dict['Lane'] for | 527 | [lane_dict['Lane'] for |
747 | 494 | lane_dict in self._archive[0]['ChildLanes']]) | 528 | lane_dict in self._archive[0]['ChildLanes']]) |
748 | 495 | self._backlog = self.connector.get( | 529 | self._backlog = self.connector.get( |
750 | 496 | "/Board/" + str(self.id) + "/Backlog").ReplyData[0] | 530 | "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0] |
751 | 497 | self._populateLanes( | 531 | self._populateLanes( |
752 | 498 | self.details['Lanes'] + archive_lanes + self._backlog) | 532 | self.details['Lanes'] + archive_lanes + self._backlog) |
753 | 533 | for card in self.cards: | ||
754 | 534 | if card.current_task_board_id: # We are a task board | ||
755 | 535 | taskboard_data = self.connector.get( | ||
756 | 536 | "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id)) | ||
757 | 537 | card.taskboard = LeankitTaskBoard( | ||
758 | 538 | taskboard_data["ReplyData"][0], self) | ||
759 | 539 | card.taskboard.fetchDetails() | ||
760 | 540 | self.cards.extend(card.taskboard.cards) | ||
761 | 499 | self._classifyCards() | 541 | self._classifyCards() |
762 | 500 | 542 | ||
763 | 501 | def _classifyCards(self): | 543 | def _classifyCards(self): |
764 | @@ -508,12 +550,16 @@ | |||
765 | 508 | self._cards_with_description_annotations.add(card) | 550 | self._cards_with_description_annotations.add(card) |
766 | 509 | if card.external_system_url: | 551 | if card.external_system_url: |
767 | 510 | self._cards_with_external_links.add(card) | 552 | self._cards_with_external_links.add(card) |
768 | 553 | if BRANCH_REGEX.match(card.external_system_url): | ||
769 | 554 | self._cards_with_branches.add(card) | ||
770 | 511 | print " - %s cards with external ids" % len( | 555 | print " - %s cards with external ids" % len( |
771 | 512 | self._cards_with_external_ids) | 556 | self._cards_with_external_ids) |
772 | 513 | print " - %s cards with external links" % len( | 557 | print " - %s cards with external links" % len( |
773 | 514 | self._cards_with_external_links) | 558 | self._cards_with_external_links) |
774 | 515 | print " - %s cards with description annotations" % len( | 559 | print " - %s cards with description annotations" % len( |
775 | 516 | self._cards_with_description_annotations) | 560 | self._cards_with_description_annotations) |
776 | 561 | print " - %s cards with branches" % len( | ||
777 | 562 | self._cards_with_branches) | ||
778 | 517 | 563 | ||
779 | 518 | def _populateUsers(self, user_data): | 564 | def _populateUsers(self, user_data): |
780 | 519 | self.users = {} | 565 | self.users = {} |
781 | @@ -625,6 +671,21 @@ | |||
782 | 625 | self._printLanes(lane, indent, include_cards) | 671 | self._printLanes(lane, indent, include_cards) |
783 | 626 | 672 | ||
784 | 627 | 673 | ||
785 | 674 | class LeankitTaskBoard(LeankitBoard): | ||
786 | 675 | |||
787 | 676 | attributes = ['Id', 'Title'] | ||
788 | 677 | |||
789 | 678 | def __init__(self, taskboard_dict, board): | ||
790 | 679 | super(LeankitTaskBoard, self).__init__(taskboard_dict, board.connector) | ||
791 | 680 | self.base_uri = '/Api/Board/%d/TaskBoard/%s/Get' % (board.id, self.id) | ||
792 | 681 | self.cardtypes = board.cardtypes | ||
793 | 682 | self.parent_board = board | ||
794 | 683 | |||
795 | 684 | def fetchDetails(self): | ||
796 | 685 | self.details = self.connector.get(self.base_uri).ReplyData[0] | ||
797 | 686 | self._populateLanes(self.details['Lanes']) | ||
798 | 687 | |||
799 | 688 | |||
800 | 628 | class LeankitKanban(object): | 689 | class LeankitKanban(object): |
801 | 629 | 690 | ||
802 | 630 | def __init__(self, account, username=None, password=None): | 691 | def __init__(self, account, username=None, password=None): |
803 | @@ -638,7 +699,7 @@ | |||
804 | 638 | 699 | ||
805 | 639 | :param include_archived: if True, include archived boards as well. | 700 | :param include_archived: if True, include archived boards as well. |
806 | 640 | """ | 701 | """ |
808 | 641 | boards_data = self.connector.get('/Boards').ReplyData | 702 | boards_data = self.connector.get('/Kanban/Api/Boards').ReplyData |
809 | 642 | boards = [] | 703 | boards = [] |
810 | 643 | for board_dict in boards_data[0]: | 704 | for board_dict in boards_data[0]: |
811 | 644 | board = LeankitBoard(board_dict, self.connector) | 705 | board = LeankitBoard(board_dict, self.connector) |
812 | @@ -683,13 +744,14 @@ | |||
813 | 683 | if __name__ == '__main__': | 744 | if __name__ == '__main__': |
814 | 684 | kanban = LeankitKanban('launchpad.leankitkanban.com', | 745 | kanban = LeankitKanban('launchpad.leankitkanban.com', |
815 | 685 | 'user@email', 'password') | 746 | 'user@email', 'password') |
816 | 747 | |||
817 | 686 | print "Active boards:" | 748 | print "Active boards:" |
818 | 687 | boards = kanban.getBoards() | 749 | boards = kanban.getBoards() |
819 | 688 | for board in boards: | 750 | for board in boards: |
820 | 689 | print " * %s (%d)" % (board.title, board.id) | 751 | print " * %s (%d)" % (board.title, board.id) |
821 | 690 | 752 | ||
822 | 691 | # Get a board by the title. | 753 | # Get a board by the title. |
824 | 692 | board_name = 'lp2kanban test' | 754 | board_name = 'Landscape 2016' |
825 | 693 | print "Getting board '%s'..." % board_name | 755 | print "Getting board '%s'..." % board_name |
826 | 694 | board = kanban.getBoard(title=board_name) | 756 | board = kanban.getBoard(title=board_name) |
827 | 695 | board.printLanes() | 757 | board.printLanes() |
828 | 696 | 758 | ||
829 | === modified file 'src/lp2kanban/tests/common.py' | |||
830 | --- src/lp2kanban/tests/common.py 2013-04-08 12:46:05 +0000 | |||
831 | +++ src/lp2kanban/tests/common.py 2015-12-16 04:22:55 +0000 | |||
832 | @@ -33,13 +33,16 @@ | |||
833 | 33 | self.is_archived = is_archived | 33 | self.is_archived = is_archived |
834 | 34 | self._cards_with_description_annotations = set() | 34 | self._cards_with_description_annotations = set() |
835 | 35 | self._cards_with_external_links = set() | 35 | self._cards_with_external_links = set() |
836 | 36 | self._cards_with_branches = set() | ||
837 | 36 | self.lanes = {} | 37 | self.lanes = {} |
838 | 37 | self.root_lane = self.addLane('ROOT LANE') | 38 | self.root_lane = self.addLane('ROOT LANE') |
839 | 38 | 39 | ||
840 | 39 | def getCardsWithDescriptionAnnotations(self): | 40 | def getCardsWithDescriptionAnnotations(self): |
841 | 40 | return self._cards_with_description_annotations | 41 | return self._cards_with_description_annotations |
842 | 41 | 42 | ||
844 | 42 | def getCardsWithExternalLinks(self): | 43 | def getCardsWithExternalLinks(self, only_branches=False): |
845 | 44 | if only_branches: | ||
846 | 45 | return self._cards_with_branches | ||
847 | 43 | return self._cards_with_external_links | 46 | return self._cards_with_external_links |
848 | 44 | 47 | ||
849 | 45 | def getLaneByPath(self, path): | 48 | def getLaneByPath(self, path): |
850 | @@ -93,7 +96,7 @@ | |||
851 | 93 | def __init__(self, external_card_id=None, title=u"", description=u"", | 96 | def __init__(self, external_card_id=None, title=u"", description=u"", |
852 | 94 | description_annotations=None, lane=None, | 97 | description_annotations=None, lane=None, |
853 | 95 | assigned_user_id=None, external_system_name=None, | 98 | assigned_user_id=None, external_system_name=None, |
855 | 96 | external_system_url=None): | 99 | external_system_url=u""): |
856 | 97 | self.external_card_id = external_card_id | 100 | self.external_card_id = external_card_id |
857 | 98 | self.title = title | 101 | self.title = title |
858 | 99 | self.description = description | 102 | self.description = description |
859 | 100 | 103 | ||
860 | === modified file 'src/lp2kanban/tests/test_bugs2cards.py' | |||
861 | --- src/lp2kanban/tests/test_bugs2cards.py 2013-04-08 12:50:46 +0000 | |||
862 | +++ src/lp2kanban/tests/test_bugs2cards.py 2015-12-16 04:22:55 +0000 | |||
863 | @@ -346,6 +346,7 @@ | |||
864 | 346 | # in CODING. | 346 | # in CODING. |
865 | 347 | self.assertEqual(CardStatus.CODING, status) | 347 | self.assertEqual(CardStatus.CODING, status) |
866 | 348 | 348 | ||
867 | 349 | BRANCH_URL = "https://code.launchpad.net/~me/project/branch-name" | ||
868 | 349 | 350 | ||
869 | 350 | class CardStatusTest(unittest.TestCase): | 351 | class CardStatusTest(unittest.TestCase): |
870 | 351 | 352 | ||
871 | @@ -375,23 +376,43 @@ | |||
872 | 375 | 376 | ||
873 | 376 | def test_should_sync_card_autosync_no(self): | 377 | def test_should_sync_card_autosync_no(self): |
874 | 377 | # When autosync is 'on', cards with no external card IDs | 378 | # When autosync is 'on', cards with no external card IDs |
876 | 378 | # are not synced. | 379 | # and no branches are not synced. |
877 | 379 | card = Record(title=u'no sync', description=u'', | 380 | card = Record(title=u'no sync', description=u'', |
879 | 380 | external_card_id=None) | 381 | external_card_id=None, external_system_url=None) |
880 | 381 | self.assertFalse(should_sync_card(card, {'sync_cards': 'on'})) | 382 | self.assertFalse(should_sync_card(card, {'sync_cards': 'on'})) |
881 | 382 | 383 | ||
883 | 383 | def test_should_sync_card_autosync_yes(self): | 384 | def test_should_sync_card_autosync_no_sync_with_non_branch_urls(self): |
884 | 385 | # When autosync is 'on', cards with external_system_urls which are not | ||
885 | 386 | # valid launchpad branches are not synced. | ||
886 | 387 | non_branch_urls = [ | ||
887 | 388 | "http://www.google.com/", | ||
888 | 389 | "https://bugs.launchpad.net/charms/+source/hacluster/+bug/1", | ||
889 | 390 | "https://code.launchpad.net/~person/project/blah/+merge/111" | ||
890 | 391 | ] | ||
891 | 392 | for url in non_branch_urls: | ||
892 | 393 | card = Record(title=u'no sync', description=u'', | ||
893 | 394 | external_card_id=None, external_system_url=url) | ||
894 | 395 | self.assertFalse(should_sync_card(card, {'sync_cards': 'on'})) | ||
895 | 396 | |||
896 | 397 | def test_should_sync_card_autosync_synced_with_external_card_id(self): | ||
897 | 384 | # When autosync is 'on', cards with external card IDs are synced. | 398 | # When autosync is 'on', cards with external card IDs are synced. |
898 | 385 | card = Record(title=u'no sync', description=u'', | 399 | card = Record(title=u'no sync', description=u'', |
900 | 386 | external_card_id='11') | 400 | external_card_id=u'11', external_system_url=u'') |
901 | 401 | self.assertTrue(should_sync_card(card, {'autosync': 'on', | ||
902 | 402 | 'sync_cards': 'on'})) | ||
903 | 403 | |||
904 | 404 | def test_should_sync_card_autosync_synced_with_branch(self): | ||
905 | 405 | # When autosync is 'on', cards with a valid branch are synced. | ||
906 | 406 | card = Record(title=u'no sync', description=u'', | ||
907 | 407 | external_card_id=u'11', external_system_url=BRANCH_URL) | ||
908 | 387 | self.assertTrue(should_sync_card(card, {'autosync': 'on', | 408 | self.assertTrue(should_sync_card(card, {'autosync': 'on', |
909 | 388 | 'sync_cards': 'on'})) | 409 | 'sync_cards': 'on'})) |
910 | 389 | 410 | ||
911 | 390 | def test_should_sync_card_autosync_nosync(self): | 411 | def test_should_sync_card_autosync_nosync(self): |
914 | 391 | # When autosync is 'on', cards with external card IDs are not | 412 | # When autosync is 'on', cards with external card IDs or a valid |
915 | 392 | # synced when they contain the no-sync marker. | 413 | # branch are not synced when they contain the no-sync marker. |
916 | 393 | card = Record(title=u'(no-sync)', description=u'(no-sync)', | 414 | card = Record(title=u'(no-sync)', description=u'(no-sync)', |
918 | 394 | external_card_id='11') | 415 | external_card_id=u'11', external_system_url=BRANCH_URL) |
919 | 395 | self.assertFalse(should_sync_card(card, {'autosync': 'on', | 416 | self.assertFalse(should_sync_card(card, {'autosync': 'on', |
920 | 396 | 'sync_cards': 'on'})) | 417 | 'sync_cards': 'on'})) |
921 | 397 | 418 | ||
922 | @@ -452,7 +473,7 @@ | |||
923 | 452 | self.assertFalse(should_sync_card(card1, conf)) | 473 | self.assertFalse(should_sync_card(card1, conf)) |
924 | 453 | self.assertFalse(should_sync_card(card2, conf)) | 474 | self.assertFalse(should_sync_card(card2, conf)) |
925 | 454 | 475 | ||
927 | 455 | def test_get_card_status_noop(self): | 476 | def test_get_card_status_noop_no_branch(self): |
928 | 456 | # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses, | 477 | # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses, |
929 | 457 | # the status is unknown. | 478 | # the status is unknown. |
930 | 458 | self.assertEqual(None, get_card_status('New', [], None)) | 479 | self.assertEqual(None, get_card_status('New', [], None)) |
931 | @@ -460,6 +481,33 @@ | |||
932 | 460 | self.assertEqual(None, get_card_status('Incomplete', [], None)) | 481 | self.assertEqual(None, get_card_status('Incomplete', [], None)) |
933 | 461 | self.assertEqual(None, get_card_status('Triaged', [], None)) | 482 | self.assertEqual(None, get_card_status('Triaged', [], None)) |
934 | 462 | 483 | ||
935 | 484 | def test_get_card_status_coding_no_bug(self): | ||
936 | 485 | # For a card with no associated bug, an in progress branch status will | ||
937 | 486 | # be in the coding state. | ||
938 | 487 | branch_info = Record(status='In Progress', target=None) | ||
939 | 488 | self.assertEqual( | ||
940 | 489 | CardStatus.CODING, | ||
941 | 490 | get_card_status(None, [], branch_info)) | ||
942 | 491 | |||
943 | 492 | def test_get_card_status_review_no_bug(self): | ||
944 | 493 | # For a card with no associated bug, an in review branch status will | ||
945 | 494 | # be in the review state. | ||
946 | 495 | branch_info = Record(status='In Review', target=None) | ||
947 | 496 | self.assertEqual( | ||
948 | 497 | CardStatus.REVIEW, | ||
949 | 498 | get_card_status(None, [], branch_info)) | ||
950 | 499 | |||
951 | 500 | def test_get_card_status_landing_no_bug(self): | ||
952 | 501 | # For a card with no associated bug, an approved or merged branch | ||
953 | 502 | # status will be in the landing state. | ||
954 | 503 | branch_infos = [ | ||
955 | 504 | Record(status='Approved', target=None), | ||
956 | 505 | Record(status='Merged', target=None)] | ||
957 | 506 | for branch_info in branch_infos: | ||
958 | 507 | self.assertEqual( | ||
959 | 508 | CardStatus.LANDING, | ||
960 | 509 | get_card_status(None, [], branch_info)) | ||
961 | 510 | |||
962 | 463 | def test_get_card_status_coding_no_branch(self): | 511 | def test_get_card_status_coding_no_branch(self): |
963 | 464 | # For a bug in 'In Progress' or 'Fix Committed' status, it is | 512 | # For a bug in 'In Progress' or 'Fix Committed' status, it is |
964 | 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. |
Please re-submit this against lp:~landscape/lp2kanban/landscape-deploy.