Merge lp:~gmb/launchpad/bugzilla3.4-see-also-bug-419134 into lp:launchpad

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~gmb/launchpad/bugzilla3.4-see-also-bug-419134
Merge into: lp:launchpad
Diff against target: 431 lines (+217/-1)
6 files modified
lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt (+32/-0)
lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt (+3/-0)
lib/lp/bugs/externalbugtracker/bugzilla.py (+34/-1)
lib/lp/bugs/tests/bugzilla-api-xmlrpc-transport.txt (+72/-0)
lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt (+6/-0)
lib/lp/bugs/tests/externalbugtracker.py (+70/-0)
To merge this branch: bzr merge lp:~gmb/launchpad/bugzilla3.4-see-also-bug-419134
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) rc Abstain
Abel Deuring (community) code Approve
Review via email: mp+15404@code.launchpad.net

Commit message

Launchpad can now update the see_also link for bugs on Bugzilla 3.4 bug trackers.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

This branch fixes bug 419134 by making the BugzillaAPI ExternalBugTracker implement the ISupportsBackLinking interface.

I've added a test implementation of the Bugzilla API's Bug.update_see_also() method, which I've then used in the tests of BugzillaAPI.setLaunchpadBugId().

This branch is an rc candidate.

= Launchpad lint =

Note: I suspect that the lint notices about email.Utils are because lint runs using the system Python (2.6) rather than bin/py.

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt
  lib/lp/bugs/externalbugtracker/bugzilla.py
  lib/lp/bugs/tests/bugzilla-api-xmlrpc-transport.txt
  lib/lp/bugs/tests/externalbugtracker.py

== Pylint notices ==

lib/lp/bugs/externalbugtracker/bugzilla.py
    20: [F0401] Unable to import 'email.Utils' (No module named Utils)

lib/lp/bugs/tests/externalbugtracker.py
    1642: [W0108, Urlib2TransportTestHandler.default_open.<lambda>] Lambda may not be necessary
    1656: [W0108, Urlib2TransportTestHandler.default_open.<lambda>] Lambda may not be necessary

Revision history for this message
Abel Deuring (adeuring) wrote :

Looks good. Please add an "XXX" to the doc string of getLaunchpadBugId(), where you mention bug 490267.

review: Approve (code)
Revision history for this message
Guilherme Salgado (salgado) wrote :

Not yet ready for RC as can't be easily QAed on staging.

review: Abstain (rc)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt'
2--- lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-28 12:54:58 +0000
3+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-12-13 01:14:17 +0000
4@@ -145,6 +145,7 @@
5 priority: P1
6 product: Marvin
7 resolution: FIXED
8+ see_also: []
9 severity: normal
10 status: RESOLVED
11 summary: That bloody robot still exists.
12@@ -161,6 +162,7 @@
13 priority: P1
14 product: HeartOfGold
15 resolution:
16+ see_also: []
17 severity: high
18 status: NEW
19 summary: Collect unknown persons in docking bay 2.
20@@ -198,6 +200,7 @@
21 priority: P1
22 product: HeartOfGold
23 resolution:
24+ see_also: []
25 severity: high
26 status: NEW
27 summary: Collect unknown persons in docking bay 2.
28@@ -560,3 +563,32 @@
29 This is a new remote comment.
30 <BLANKLINE>
31
32+
33+Linking a Launchpad bug to a remote bug
34+---------------------------------------
35+
36+BugzillaAPI implements the ISupportsBackLinking interface, which means
37+that it can be used to tell the remote bug tracker that a given remote
38+bug is linked to a Launchpad bug.
39+
40+ >>> from canonical.launchpad.interfaces import ISupportsBackLinking
41+ >>> verifyObject(ISupportsBackLinking, bugzilla)
42+ True
43+
44+BugzillaAPI.setLaunchpadBugId() can be used to set the Launchpad bug ID
45+for a given bug.
46+
47+setLaunchpadBugId() requires the user to be logged in.
48+
49+ >>> bugzilla.xmlrpc_transport.expireCookie(
50+ ... bugzilla.xmlrpc_transport.auth_cookie)
51+
52+ >>> bugzilla.xmlrpc_transport.print_method_calls = True
53+ >>> bugzilla.setLaunchpadBugId(bug_watch.remotebug, bug_watch.bug.id)
54+ CALLED Bug.update_see_also({'add':
55+ ['http://bugs.launchpad.dev/bugs...'], 'ids': [1]})
56+
57+BugzillaAPI.getLaunchpadBugId() will currently always return None due to
58+bug 490267.
59+
60+ >>> bugzilla.getLaunchpadBugId(bug_watch.remotebug)
61
62=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt'
63--- lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-26 00:17:29 +0000
64+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-12-13 01:14:17 +0000
65@@ -210,6 +210,7 @@
66 priority: P1
67 product: Marvin
68 resolution: FIXED
69+ see_also:...
70 severity: normal
71 status: RESOLVED
72 summary: That bloody robot still exists.
73@@ -226,6 +227,7 @@
74 priority: P1
75 product: HeartOfGold
76 resolution:
77+ see_also:...
78 severity: high
79 status: NEW
80 summary: Collect unknown persons in docking bay 2.
81@@ -555,6 +557,7 @@
82 priority: P1
83 product: HeartOfGold
84 resolution:
85+ see_also: []
86 severity: high
87 status: NEW
88 summary: Collect unknown persons in docking bay 2.
89
90=== modified file 'lib/lp/bugs/externalbugtracker/bugzilla.py'
91--- lib/lp/bugs/externalbugtracker/bugzilla.py 2009-09-23 12:24:10 +0000
92+++ lib/lp/bugs/externalbugtracker/bugzilla.py 2009-12-13 01:14:17 +0000
93@@ -27,6 +27,7 @@
94 from canonical.config import config
95 from canonical.launchpad.interfaces.message import IMessageSet
96 from canonical.launchpad.webapp.url import urlappend, urlparse
97+from canonical.launchpad.webapp.publisher import canonical_url
98
99 from lp.bugs.externalbugtracker.base import (
100 BugNotFound, BugTrackerAuthenticationError, BugTrackerConnectError,
101@@ -35,6 +36,7 @@
102 UnparseableBugTrackerVersion)
103 from lp.bugs.externalbugtracker.xmlrpc import (
104 UrlLib2Transport)
105+from lp.bugs.interfaces.bug import IBugSet
106 from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
107 from lp.bugs.interfaces.externalbugtracker import UNKNOWN_REMOTE_IMPORTANCE
108 from lp.bugs.interfaces.externalbugtracker import (
109@@ -425,7 +427,8 @@
110 class BugzillaAPI(Bugzilla):
111 """An `ExternalBugTracker` to handle Bugzillas that offer an API."""
112
113- implements(ISupportsCommentImport, ISupportsCommentPushing)
114+ implements(
115+ ISupportsBackLinking, ISupportsCommentImport, ISupportsCommentPushing)
116
117 def __init__(self, baseurl, xmlrpc_transport=None,
118 internal_xmlrpc_transport=None):
119@@ -756,6 +759,36 @@
120 # BugWatchUpdater will expect (see bug 248938).
121 return str(return_dict['id'])
122
123+ def getLaunchpadBugId(self, remote_bug):
124+ """Return the Launchpad bug ID for the remote bug.
125+
126+ See `ISupportsBackLinking`.
127+ """
128+ # XXX gmb 2009-11-30 bug=490267
129+ # In fact, this method always returns None due to bug
130+ # 490267. Once the bug is fixed in Bugzilla we should update
131+ # this method.
132+ return None
133+
134+ @needs_authentication
135+ def setLaunchpadBugId(self, remote_bug, launchpad_bug_id):
136+ """Set the Launchpad bug for a given remote bug.
137+
138+ See `ISupportsBackLinking`.
139+ """
140+ actual_bug_id = self._getActualBugId(remote_bug)
141+
142+ # Grab the bug from the database and get its canonical URL.
143+ launchpad_bug = getUtility(IBugSet).get(launchpad_bug_id)
144+ launchpad_bug_url = canonical_url(launchpad_bug)
145+
146+ request_params = {
147+ 'ids': [actual_bug_id],
148+ 'add': [launchpad_bug_url],
149+ }
150+
151+ self.xmlrpc_proxy.Bug.update_see_also(request_params)
152+
153
154 class BugzillaLPPlugin(BugzillaAPI):
155 """An `ExternalBugTracker` to handle Bugzillas using the LP Plugin."""
156
157=== modified file 'lib/lp/bugs/tests/bugzilla-api-xmlrpc-transport.txt'
158--- lib/lp/bugs/tests/bugzilla-api-xmlrpc-transport.txt 2009-09-02 09:18:14 +0000
159+++ lib/lp/bugs/tests/bugzilla-api-xmlrpc-transport.txt 2009-12-13 01:14:17 +0000
160@@ -113,6 +113,7 @@
161 priority: P1
162 product: Marvin
163 resolution: FIXED
164+ see_also: []
165 severity: normal
166 status: RESOLVED
167 summary: That bloody robot still exists.
168@@ -165,6 +166,7 @@
169 priority: P1
170 product: HeartOfGold
171 resolution:
172+ see_also: []
173 severity: high
174 status: NEW
175 summary: Collect unknown persons in docking bay 2.
176@@ -194,6 +196,7 @@
177 priority: P1
178 product: Marvin
179 resolution: FIXED
180+ see_also: []
181 severity: normal
182 status: RESOLVED
183 summary: That bloody robot still exists.
184@@ -237,6 +240,7 @@
185 priority: P1
186 product: HeartOfGold
187 resolution:
188+ see_also: []
189 severity: high
190 status: NEW
191 summary: Collect unknown persons in docking bay 2.
192@@ -261,6 +265,7 @@
193 priority: P1
194 product: Marvin
195 resolution: FIXED
196+ see_also: []
197 severity: normal
198 status: RESOLVED
199 summary: That bloody robot still exists.
200@@ -275,6 +280,7 @@
201 priority: P1
202 product: HeartOfGold
203 resolution:
204+ see_also: []
205 severity: high
206 status: NEW
207 summary: Collect unknown persons in docking bay 2.
208@@ -469,3 +475,69 @@
209 Fault: <Fault 101: 'Bug #42 does not exist.'>
210
211
212+Updating the "See also" links on a bug
213+--------------------------------------
214+
215+It's possible to alter the list of bugs linked to a bug in a Bugzilla
216+instance by calling the Bug.update_see_also() method.
217+
218+URLs can be added to the list of "See also" links by passing them in the
219+`add` parameter.
220+
221+ >>> server.Bug.update_see_also({
222+ ... 'ids': [1], 'add': ['https://launchpad.net/bugs/15']})
223+ {'changes': {1: {'see_also':
224+ {'added': ['https://launchpad.net/bugs/15']}}}}
225+
226+The URL will now have been added to the bug's see_also list.
227+
228+ >>> return_value = server.Bug.get(
229+ ... {'ids': [1], 'permissive': True})
230+ >>> bug_dict = return_value['bugs'][0]
231+ >>> for key in sorted(bug_dict):
232+ ... print "%s: %s" % (key, bug_dict.get(key))
233+ alias:
234+ assigned_to: test@canonical.com
235+ component: GPPSystems
236+ creation_time: 20080610T16:19:53
237+ id: 1
238+ internals:...
239+ is_open: True
240+ last_change_time: 20080610T16:19:53
241+ priority: P1
242+ product: Marvin
243+ resolution: FIXED
244+ see_also: ['https://launchpad.net/bugs/15']
245+ severity: normal
246+ status: RESOLVED
247+ summary: That bloody robot still exists.
248+
249+Any attempt to add the same URL again will simply be ignored.
250+
251+ >>> server.Bug.update_see_also({
252+ ... 'ids': [1], 'add': ['https://launchpad.net/bugs/15']})
253+ {'changes': {}}
254+
255+Trying to add a non Bugzilla or Launchpad URL will raise a Fault.
256+
257+ >>> server.Bug.update_see_also({
258+ ... 'ids': [1], 'add': ['http://example.com/fail']});
259+ Traceback (most recent call last):
260+ ...
261+ Fault: <Fault 112: 'Bug URL http://example.com/fail is invalid.'>
262+
263+It's also possible to remove items from a bug's see_also list.
264+
265+ >>> server.Bug.update_see_also({
266+ ... 'ids': [1], 'remove': ['https://launchpad.net/bugs/15']})
267+ {'changes': {1: {'see_also':
268+ {'removed': ['https://launchpad.net/bugs/15']}}}}
269+
270+If a URL is passed in both the `add` and `remove` argument, it will be
271+added (i.e. `add` overrides `remove`).
272+
273+ >>> server.Bug.update_see_also({
274+ ... 'ids': [1], 'add': ['https://launchpad.net/bugs/14'],
275+ ... 'remove': ['https://launchpad.dev/bugs/14']})
276+ {'changes': {1: {'see_also':
277+ {'added': ['https://launchpad.net/bugs/14']}}}}
278
279=== modified file 'lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt'
280--- lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-08-21 13:13:56 +0000
281+++ lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-12-13 01:14:17 +0000
282@@ -152,6 +152,7 @@
283 priority: P1
284 product: Marvin
285 resolution: FIXED
286+ see_also: []
287 severity: normal
288 status: RESOLVED
289 summary: That bloody robot still exists.
290@@ -177,6 +178,7 @@
291 priority: P1
292 product: Marvin
293 resolution: FIXED
294+ see_also: []
295 severity: normal
296 status: RESOLVED
297 summary: That bloody robot still exists.
298@@ -192,6 +194,7 @@
299 priority: P1
300 product: HeartOfGold
301 resolution:
302+ see_also: []
303 severity: high
304 status: NEW
305 summary: Collect unknown persons in docking bay 2.
306@@ -214,6 +217,7 @@
307 priority: P1
308 product: HeartOfGold
309 resolution:
310+ see_also: []
311 severity: high
312 status: NEW
313 summary: Collect unknown persons in docking bay 2.
314@@ -258,6 +262,7 @@
315 priority: P1
316 product: HeartOfGold
317 resolution:
318+ see_also: []
319 severity: high
320 status: NEW
321 summary: Collect unknown persons in docking bay 2.
322@@ -307,6 +312,7 @@
323 priority: P1
324 product: HeartOfGold
325 resolution:
326+ see_also: []
327 severity: high
328 status: NEW
329 summary: Collect unknown persons in docking bay 2.
330
331=== modified file 'lib/lp/bugs/tests/externalbugtracker.py'
332--- lib/lp/bugs/tests/externalbugtracker.py 2009-09-02 08:32:27 +0000
333+++ lib/lp/bugs/tests/externalbugtracker.py 2009-12-13 01:14:17 +0000
334@@ -379,6 +379,7 @@
335 'priority': 'P1',
336 'product': 'Marvin',
337 'resolution': 'FIXED',
338+ 'see_also': [],
339 'severity': 'normal',
340 'status': 'RESOLVED',
341 'summary': "That bloody robot still exists.",
342@@ -394,6 +395,7 @@
343 'priority': 'P1',
344 'product': 'HeartOfGold',
345 'resolution': '',
346+ 'see_also': [],
347 'severity': 'high',
348 'status': 'NEW',
349 'summary': 'Collect unknown persons in docking bay 2.',
350@@ -773,6 +775,7 @@
351 'comments',
352 'get',
353 'search',
354+ 'update_see_also',
355 ],
356 'Bugzilla': [
357 'time',
358@@ -992,6 +995,73 @@
359 # cause it to explode.
360 return [{'id': comment_id}]
361
362+ def update_see_also(self, arguments):
363+ """Update the see_also references for a bug."""
364+ assert 'ids' in arguments, (
365+ "You must specify a set of IDs with which to work.")
366+ assert ('add' in arguments or 'remove' in arguments), (
367+ "You must specify a list of links to add or remove.")
368+
369+ changes = {}
370+
371+ for bug_id in arguments['ids']:
372+ bug_id = int(bug_id)
373+
374+ # If the bug ID doesn't exist, raise a Fault.
375+ if bug_id not in self.bugs:
376+ raise xmlrpclib.Fault(101, "Bug #%s does not exist." % bug_id)
377+
378+ see_also_list = self.bugs[bug_id].get('see_also', [])
379+
380+ # Remove any items first. That way, if they're also in the
381+ # 'add' section they'll get re-added.
382+ for url in arguments.get('remove', []):
383+ if url not in see_also_list:
384+ continue
385+
386+ if changes.get(bug_id) is None:
387+ changes[bug_id] = {}
388+
389+ if changes[bug_id].get('see_also') is None:
390+ changes[bug_id]['see_also'] = {}
391+
392+ if changes[bug_id]['see_also'].get('removed') is None:
393+ changes[bug_id]['see_also']['removed'] = []
394+
395+ see_also_list.remove(url)
396+ changes[bug_id]['see_also']['removed'].append(url)
397+
398+ # Add any items to the list.
399+ for url in arguments.get('add', []):
400+ if url in see_also_list:
401+ # Ignore existing urls.
402+ continue
403+
404+ if ('launchpad' not in url and
405+ 'show_bug.cgi' not in url):
406+ raise xmlrpclib.Fault(
407+ 112, "Bug URL %s is invalid." % url)
408+
409+ if changes.get(bug_id) is None:
410+ changes[bug_id] = {}
411+
412+ if changes[bug_id].get('see_also') is None:
413+ changes[bug_id]['see_also'] = {}
414+
415+ if changes[bug_id]['see_also'].get('added') is None:
416+ changes[bug_id]['see_also']['added'] = []
417+
418+ see_also_list.append(url)
419+ changes[bug_id]['see_also']['added'].append(url)
420+
421+ # Replace the bug's existing see_also list.
422+ self.bugs[bug_id]['see_also'] = see_also_list
423+
424+ # We have to return a list here because xmlrpclib will try to
425+ # expand sequences of length 1. Trying to do that on a dict will
426+ # cause it to explode.
427+ return [{'changes': changes}]
428+
429
430 class TestMantis(Mantis):
431 """Mantis ExternalSystem for use in tests.