~launchpad-pqm/launchpad/devel

12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
3
4
"""Bugzilla ExternalBugTracker utility."""
5
6
__metaclass__ = type
6290.4.8 by Graham Binns
Review changes for barry.
7
__all__ = [
8
    'Bugzilla',
9150.2.2 by Graham Binns
Add a BugzillaAPI ExternalBugTracker.
9
    'BugzillaAPI',
6290.4.8 by Graham Binns
Review changes for barry.
10
    'BugzillaLPPlugin',
6532.1.10 by Graham Binns
Added tests for @needs_authentication.
11
    'needs_authentication',
6290.4.8 by Graham Binns
Review changes for barry.
12
    ]
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
13
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
14
from email.Utils import parseaddr
10136.3.2 by Gavin Panella
Make _parseVersion() just pick out numbers from the version string.
15
import re
12599.4.2 by Leonard Richardson
Merge from trunk.
16
from httplib import BadStatusLine
17
from urllib2 import URLError
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
18
from xml.dom import minidom
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
19
import xml.parsers.expat
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
20
import xmlrpclib
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
21
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
import pytz
6506.5.8 by Graham Binns
Added tests and implementation for getMessageForComment().
23
from zope.component import getUtility
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
24
from zope.interface import implements
25
6532.1.3 by Graham Binns
Added tests for _authenticate.
26
from canonical.config import config
12929.9.2 by j.c.sackett
Moved messages from canonical to lp.services
27
from lp.services.messages.interfaces.message import IMessageSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
28
from canonical.launchpad.webapp.url import (
29
    urlappend,
30
    urlparse,
31
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
32
from lp.bugs.externalbugtracker.base import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
33
    BugNotFound,
34
    BugTrackerAuthenticationError,
35
    BugTrackerConnectError,
36
    ExternalBugTracker,
37
    InvalidBugId,
38
    LookupTree,
11403.1.6 by Henning Eggers
Merged devel r11406, resolved conflict.
39
    UnknownRemoteImportanceError,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
40
    UnknownRemoteStatusError,
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
41
    UnparsableBugData,
42
    UnparsableBugTrackerVersion,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
43
    )
44
from lp.bugs.externalbugtracker.xmlrpc import UrlLib2Transport
45
from lp.bugs.interfaces.bugtask import (
46
    BugTaskImportance,
47
    BugTaskStatus,
48
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
49
from lp.bugs.interfaces.externalbugtracker import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
50
    ISupportsBackLinking,
51
    ISupportsCommentImport,
52
    ISupportsCommentPushing,
53
    UNKNOWN_REMOTE_IMPORTANCE,
54
    )
12398.2.15 by Jonathan Lange
Fix some more imports.
55
from lp.services import encoding
12579.1.1 by William Grant
Move lp.bugs.externalbugtracker.isolation to lp.services.database. It's not Bugs-specific.
56
from lp.services.database.isolation import ensure_no_transaction
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
57
5863.5.5 by Graham Binns
Minor tweak.
58
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
59
class Bugzilla(ExternalBugTracker):
60
    """An ExternalBugTrack for dealing with remote Bugzilla systems."""
61
62
    batch_query_threshold = 0 # Always use the batch method.
6604.1.18 by Tom Berger
if a test proxy member is present use it (only used in tests)
63
    _test_xmlrpc_proxy = None
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
64
65
    def __init__(self, baseurl, version=None):
66
        super(Bugzilla, self).__init__(baseurl)
67
        self.version = self._parseVersion(version)
68
        self.is_issuezilla = False
69
        self.remote_bug_status = {}
11304.1.4 by Bryce Harrington
Somewhat cleaner stubbing out of remote bug importance initting
70
        self.remote_bug_importance = {}
7781.1.3 by Bjorn Tillenius
Implement Bugzilla.getRemoteProduct().
71
        self.remote_bug_product = {}
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
72
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
73
    @ensure_no_transaction
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
74
    def _remoteSystemHasBugzillaAPI(self):
75
        """Return True if the remote host offers the Bugzilla API.
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
76
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
77
        :return: True if the remote host offers an XML-RPC API and its
78
            version is > 3.4. Return False otherwise.
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
79
        """
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
80
        api = BugzillaAPI(self.baseurl)
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
81
        if self._test_xmlrpc_proxy is not None:
82
            proxy = self._test_xmlrpc_proxy
83
        else:
84
            proxy = api.xmlrpc_proxy
85
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
86
        try:
87
            # We try calling Bugzilla.version() on the remote
88
            # server because it's the most lightweight method there is.
10136.3.3 by Gavin Panella
Rename remote_version_dict to remote_version, because it might be a tuple. Suggested by bac in review.
89
            remote_version = proxy.Bugzilla.version()
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
90
        except xmlrpclib.Fault, fault:
10553.2.1 by Gavin Panella
Treat the fault code 'Client' the same as METHOD_NOT_FOUND when talking to Bugzilla over XML-RPC.
91
            # 'Client' is a hangover. Either Bugzilla or the Perl
92
            # XML-RPC lib in use returned it as faultCode. It's wrong,
93
            # but it's known wrongness, so we recognize it here.
94
            if fault.faultCode in (xmlrpclib.METHOD_NOT_FOUND, 'Client'):
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
95
                return False
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
96
            else:
97
                raise
98
        except xmlrpclib.ProtocolError, error:
99
            # We catch 404s, which occur when xmlrpc.cgi doesn't exist
100
            # on the remote server, and 500s, which sometimes occur when
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
101
            # an invalid request is made to the remote server. We allow
102
            # any other error types to propagate upward.
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
103
            if error.errcode in (404, 500):
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
104
                return False
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
105
            else:
106
                raise
12336.3.1 by Gavin Panella
Resurrect all the externalbugtracker stuff.
107
        except (xmlrpclib.ResponseError, xml.parsers.expat.ExpatError):
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
108
            # The server returned an unparsable response.
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
109
            return False
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
110
        else:
10136.3.1 by Gavin Panella
Check that the version information returned by the Bugzilla API is a mapping.
111
            # Older versions of the Bugzilla API return tuples. We
112
            # consider anything other than a mapping to be unsupported.
10136.3.3 by Gavin Panella
Rename remote_version_dict to remote_version, because it might be a tuple. Suggested by bac in review.
113
            if isinstance(remote_version, dict):
114
                if remote_version['version'] >= '3.4':
10136.3.1 by Gavin Panella
Check that the version information returned by the Bugzilla API is a mapping.
115
                    return True
116
            return False
9570.2.2 by Graham Binns
BugzillaAPI instances are now detected properly. Of course, everything else is broken.
117
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
118
    @ensure_no_transaction
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
119
    def _remoteSystemHasPluginAPI(self):
120
        """Return True if the remote host has the Launchpad plugin installed.
121
        """
6604.1.15 by Tom Berger
use the same plugin instance for probing and for communicating later ; also, only establish the xmlrpc endpoint in the class that actually uses it
122
        plugin = BugzillaLPPlugin(self.baseurl)
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
123
        if self._test_xmlrpc_proxy is not None:
124
            proxy = self._test_xmlrpc_proxy
125
        else:
126
            proxy = plugin.xmlrpc_proxy
127
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
128
        try:
129
            # We try calling Launchpad.plugin_version() on the remote
130
            # server because it's the most lightweight method there is.
6604.1.18 by Tom Berger
if a test proxy member is present use it (only used in tests)
131
            proxy.Launchpad.plugin_version()
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
132
        except xmlrpclib.Fault, fault:
10553.2.1 by Gavin Panella
Treat the fault code 'Client' the same as METHOD_NOT_FOUND when talking to Bugzilla over XML-RPC.
133
            # 'Client' is a hangover. Either Bugzilla or the Perl
134
            # XML-RPC lib in use returned it as faultCode. It's wrong,
135
            # but it's known wrongness, so we recognize it here.
136
            if fault.faultCode in (xmlrpclib.METHOD_NOT_FOUND, 'Client'):
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
137
                return False
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
138
            else:
139
                raise
140
        except xmlrpclib.ProtocolError, error:
6645.1.1 by Graham Binns
Added tests and a fix for bug 246285.
141
            # We catch 404s, which occur when xmlrpc.cgi doesn't exist
142
            # on the remote server, and 500s, which sometimes occur when
143
            # the Launchpad Plugin isn't installed. Everything else we
144
            # can consider to be a problem, so we let it travel up the
145
            # stack for the error log.
146
            if error.errcode in (404, 500):
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
147
                return False
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
148
            else:
149
                raise
12336.3.1 by Gavin Panella
Resurrect all the externalbugtracker stuff.
150
        except (xmlrpclib.ResponseError, xml.parsers.expat.ExpatError):
6715.1.1 by Bjorn Tillenius
catch ResponseError when checking if the LP plugin is installed on bugzilla.
151
            # The server returned an unparsable response.
9570.2.3 by Graham Binns
Bugzilla.getExternalBugTrackerToUse() now knows how to recognise a Bugzilla with an API.
152
            return False
153
        else:
154
            return True
155
156
    def getExternalBugTrackerToUse(self):
157
        """Return the correct `Bugzilla` subclass for the current bugtracker.
158
159
        See `IExternalBugTracker`.
160
        """
12599.4.2 by Leonard Richardson
Merge from trunk.
161
        # checkwatches isn't set up to handle errors here, so we supress
162
        # known connection issues. They'll be handled and logged later on when
163
        # further requests are attempted.
164
        try:
165
            if self._remoteSystemHasPluginAPI():
166
                return BugzillaLPPlugin(self.baseurl)
167
            elif self._remoteSystemHasBugzillaAPI():
168
                return BugzillaAPI(self.baseurl)
169
        except (xmlrpclib.ProtocolError, URLError, BadStatusLine):
170
            pass
171
        return self
6570.1.1 by Graham Binns
Added tests and implementation for Bugzilla.getExternalBugTrackerToUse().
172
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
173
    def _parseDOMString(self, contents):
174
        """Return a minidom instance representing the XML contents supplied"""
175
        # Some Bugzilla sites will return pages with content that has
176
        # broken encoding. It's unfortunate but we need to guess the
177
        # encoding that page is in, and then encode() it into the utf-8
178
        # that minidom requires.
179
        contents = encoding.guess(contents).encode("utf-8")
180
        return minidom.parseString(contents)
181
182
    def _probe_version(self):
183
        """Retrieve and return a remote bugzilla version.
184
185
        If the version cannot be parsed from the remote server
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
186
        `UnparsableBugTrackerVersion` will be raised. If the remote
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
187
        server cannot be reached `BugTrackerConnectError` will be
188
        raised.
189
        """
190
        version_xml = self._getPage('xml.cgi?id=1')
191
        try:
192
            document = self._parseDOMString(version_xml)
193
        except xml.parsers.expat.ExpatError, e:
194
            raise BugTrackerConnectError(self.baseurl,
195
                "Failed to parse output when probing for version: %s" % e)
196
        bugzilla = document.getElementsByTagName("bugzilla")
197
        if not bugzilla:
198
            # Welcome to Disneyland. The Issuezilla tracker replaces
199
            # "bugzilla" with "issuezilla".
200
            bugzilla = document.getElementsByTagName("issuezilla")
201
            if bugzilla:
202
                self.is_issuezilla = True
203
            else:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
204
                raise UnparsableBugTrackerVersion(
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
205
                    'Failed to parse version from xml.cgi for %s: could '
206
                    'not find top-level bugzilla element'
207
                    % self.baseurl)
208
        version = bugzilla[0].getAttribute("version")
209
        return self._parseVersion(version)
210
211
    def _parseVersion(self, version):
212
        """Return a Bugzilla version parsed into a tuple.
213
214
        A typical tuple will be in the form (major_version,
215
        minor_version), so the version string '2.15' would be returned
216
        as (2, 15).
217
218
        If the passed version is None, None will be returned.
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
219
        If the version cannot be parsed `UnparsableBugTrackerVersion`
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
220
        will be raised.
221
        """
222
        if version is None:
223
            return None
224
10136.3.2 by Gavin Panella
Make _parseVersion() just pick out numbers from the version string.
225
        version_numbers = re.findall('[0-9]+', version)
226
        if len(version_numbers) == 0:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
227
            raise UnparsableBugTrackerVersion(
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
228
                'Failed to parse version %r for %s' %
229
                (version, self.baseurl))
230
10136.3.2 by Gavin Panella
Make _parseVersion() just pick out numbers from the version string.
231
        return tuple(int(number) for number in version_numbers)
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
232
11304.1.3 by Bryce Harrington
Stub in convertRemoteImportance()
233
    _importance_lookup = {
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
234
        'blocker': BugTaskImportance.CRITICAL,
235
        'critical': BugTaskImportance.CRITICAL,
236
        'immediate': BugTaskImportance.CRITICAL,
237
        'urgent': BugTaskImportance.CRITICAL,
12435.1.2 by William Grant
Add OOo priorities, since they have no separate severity field.
238
        'p5': BugTaskImportance.CRITICAL,
12435.1.1 by William Grant
Add additional KDE severities.
239
        'crash': BugTaskImportance.HIGH,
240
        'grave': BugTaskImportance.HIGH,
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
241
        'major': BugTaskImportance.HIGH,
242
        'high': BugTaskImportance.HIGH,
12435.1.2 by William Grant
Add OOo priorities, since they have no separate severity field.
243
        'p4': BugTaskImportance.HIGH,
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
244
        'normal': BugTaskImportance.MEDIUM,
245
        'medium': BugTaskImportance.MEDIUM,
12435.1.2 by William Grant
Add OOo priorities, since they have no separate severity field.
246
        'p3': BugTaskImportance.MEDIUM,
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
247
        'minor': BugTaskImportance.LOW,
248
        'low': BugTaskImportance.LOW,
249
        'trivial': BugTaskImportance.LOW,
12435.1.2 by William Grant
Add OOo priorities, since they have no separate severity field.
250
        'p2': BugTaskImportance.LOW,
251
        'p1': BugTaskImportance.LOW,
11304.1.22 by Bryce Harrington
Cleanup a heapload of lint errors
252
        'enhancement': BugTaskImportance.WISHLIST,
12435.1.1 by William Grant
Add additional KDE severities.
253
        'wishlist': BugTaskImportance.WISHLIST,
11304.1.3 by Bryce Harrington
Stub in convertRemoteImportance()
254
        }
255
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
256
    def convertRemoteImportance(self, remote_importance):
11304.1.21 by Bryce Harrington
Define UnknownRemoteImportanceError class
257
        """See `ExternalBugTracker`."""
11629.1.4 by Bryce Harrington
Handle case of bugzillas which can return '' for priority and severity.
258
        words = remote_importance.lower().split()
11304.1.7 by Bryce Harrington
Implement convertRemoteImportance()
259
        try:
11629.1.4 by Bryce Harrington
Handle case of bugzillas which can return '' for priority and severity.
260
            return self._importance_lookup[words.pop()]
11304.1.7 by Bryce Harrington
Implement convertRemoteImportance()
261
        except KeyError:
262
            raise UnknownRemoteImportanceError(remote_importance)
11629.1.4 by Bryce Harrington
Handle case of bugzillas which can return '' for priority and severity.
263
        except IndexError:
264
            return BugTaskImportance.UNKNOWN
11304.1.7 by Bryce Harrington
Implement convertRemoteImportance()
265
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
266
        return BugTaskImportance.UNKNOWN
267
6326.8.20 by Gavin Panella
Add titles.
268
    _status_lookup_titles = 'Bugzilla status', 'Bugzilla resolution'
6326.8.34 by Gavin Panella
Rename Lookup to LookupTree in the externalbugtracker modules.
269
    _status_lookup = LookupTree(
6326.8.11 by Gavin Panella
More cleanups.
270
        ('ASSIGNED', 'ON_DEV', 'FAILS_QA', 'STARTED',
271
         BugTaskStatus.INPROGRESS),
12435.1.4 by William Grant
KDE uses NEEDSINFO instead of NEEDINFO.
272
        ('NEEDINFO', 'NEEDINFO_REPORTER', 'NEEDSINFO', 'WAITING', 'SUSPENDED',
11411.7.24 by j.c.sackett
Merged from devel.
273
         'PLEASETEST',
6326.8.11 by Gavin Panella
More cleanups.
274
         BugTaskStatus.INCOMPLETE),
275
        ('PENDINGUPLOAD', 'MODIFIED', 'RELEASE_PENDING', 'ON_QA',
276
         BugTaskStatus.FIXCOMMITTED),
277
        ('REJECTED', BugTaskStatus.INVALID),
6326.8.59 by Gavin Panella
A few small post-review changes.
278
        ('RESOLVED', 'VERIFIED', 'CLOSED',
279
            LookupTree(
6326.8.11 by Gavin Panella
More cleanups.
280
                ('CODE_FIX', 'CURRENTRELEASE', 'ERRATA', 'NEXTRELEASE',
11411.7.24 by j.c.sackett
Merged from devel.
281
                 'PATCH_ALREADY_AVAILABLE', 'FIXED', 'RAWHIDE',
282
                 'DOCUMENTED',
6326.8.11 by Gavin Panella
More cleanups.
283
                 BugTaskStatus.FIXRELEASED),
11411.7.24 by j.c.sackett
Merged from devel.
284
                ('WONTFIX', 'WILL_NOT_FIX', 'NOTOURBUG', 'UPSTREAM',
285
                 BugTaskStatus.WONTFIX),
286
                ('OBSOLETE', 'INSUFFICIENT_DATA', 'INCOMPLETE', 'EXPIRED',
287
                 BugTaskStatus.EXPIRED),
288
                ('INVALID', 'WORKSFORME', 'NOTABUG', 'CANTFIX',
12435.1.3 by William Grant
A Bugzilla resolution of DUPLICATE is Invalid until we can handle duplicates directly.
289
                 'UNREPRODUCIBLE', 'DUPLICATE',
12435.1.5 by William Grant
Don't explicitly map unknown RESOLVED/CLOSED/VERIFIED statuses to Unknown. Let the exception bubble up so it gets logged.
290
                 BugTaskStatus.INVALID))),
11411.7.24 by j.c.sackett
Merged from devel.
291
        ('REOPENED', 'NEW', 'UPSTREAM', 'DEFERRED',
292
         BugTaskStatus.CONFIRMED),
6326.8.11 by Gavin Panella
More cleanups.
293
        ('UNCONFIRMED', BugTaskStatus.NEW),
294
        )
295
296
    def convertRemoteStatus(self, remote_status):
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
297
        """See `IExternalBugTracker`.
298
299
        Bugzilla status consist of two parts separated by space, where
300
        the last part is the resolution. The resolution is optional.
301
        """
6326.8.6 by Gavin Panella
Convert bugzilla.
302
        try:
6326.8.41 by Gavin Panella
Change __call__ to find in externalbugtracker too.
303
            return self._status_lookup.find(*remote_status.split())
6326.8.6 by Gavin Panella
Convert bugzilla.
304
        except KeyError:
6326.8.11 by Gavin Panella
More cleanups.
305
            raise UnknownRemoteStatusError(remote_status)
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
306
307
    def initializeRemoteBugDB(self, bug_ids):
308
        """See `ExternalBugTracker`.
309
310
        This method is overriden so that Bugzilla version issues can be
311
        accounted for.
312
        """
313
        if self.version is None:
314
            self.version = self._probe_version()
315
316
        super(Bugzilla, self).initializeRemoteBugDB(bug_ids)
317
318
    def getRemoteBug(self, bug_id):
319
        """See `ExternalBugTracker`."""
320
        return (bug_id, self.getRemoteBugBatch([bug_id]))
321
12221.1.4 by Jeroen Vermeulen
Check that bugzilla search result really looks like a search result page.
322
    def _checkBugSearchResult(self, document):
323
        """Does `document` appear to be a bug search result page?
324
325
        :param document: An `xml.dom.Document` built from a bug search result
326
            on the bugzilla instance.
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
327
        :raise UnparsableBugData: If `document` does not appear to be a bug
12221.1.4 by Jeroen Vermeulen
Check that bugzilla search result really looks like a search result page.
328
            search result.
329
        """
330
        root = document.documentElement
12221.1.5 by Jeroen Vermeulen
Be a bit more liberal about bugzilla search result root tags: forbid only HTML.
331
        if root.tagName == 'html':
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
332
            raise UnparsableBugData(
12221.1.4 by Jeroen Vermeulen
Check that bugzilla search result really looks like a search result page.
333
                "Bug search on %s returned a <%s> instead of an RDF page." % (
334
                    self.baseurl, root.tagName))
335
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
336
    def getRemoteBugBatch(self, bug_ids):
337
        """See `ExternalBugTracker`."""
338
        # XXX: GavinPanella 2007-10-25 bug=153532: The modification of
339
        # self.remote_bug_status later on is a side-effect that should
340
        # really not be in this method, but for the fact that
341
        # getRemoteStatus needs it at other times. Perhaps
342
        # getRemoteBug and getRemoteBugBatch could return RemoteBug
343
        # objects which have status properties that would replace
344
        # getRemoteStatus.
345
        if self.is_issuezilla:
346
            buglist_page = 'xml.cgi'
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
347
            data = {
348
                'download_type': 'browser',
349
                'output_configured': 'true',
350
                'include_attachments': 'false',
351
                'include_dtd': 'true',
352
                'id': ','.join(bug_ids),
353
                }
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
354
            bug_tag = 'issue'
355
            id_tag = 'issue_id'
356
            status_tag = 'issue_status'
357
            resolution_tag = 'resolution'
11304.1.12 by Bryce Harrington
Implement retrieving priority+severity from bugzilla into importance
358
            priority_tag = 'priority'
11304.1.18 by Bryce Harrington
Further handling of IssueZilla's lack of severity field
359
            severity_tag = None
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
360
        elif self.version < (2, 16):
361
            buglist_page = 'xml.cgi'
362
            data = {'id': ','.join(bug_ids)}
363
            bug_tag = 'bug'
364
            id_tag = 'bug_id'
365
            status_tag = 'bug_status'
366
            resolution_tag = 'resolution'
11304.1.12 by Bryce Harrington
Implement retrieving priority+severity from bugzilla into importance
367
            priority_tag = 'priority'
11304.1.13 by Bryce Harrington
Looks like the bugzilla term is 'bug_severity'. Go consistency.
368
            severity_tag = 'bug_severity'
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
369
        else:
370
            buglist_page = 'buglist.cgi'
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
371
            data = {
372
                'form_name': 'buglist.cgi',
373
                'bug_id_type': 'include',
12266.2.1 by Gavin Panella
For some reason bugs.freedesktop.org (Bugzilla 3.4.6) has decided to take notice of the columnlist parameter we pass over.
374
                'columnlist':
375
                    ('id,product,bug_status,resolution,'
376
                     'priority,bug_severity'),
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
377
                'bug_id': ','.join(bug_ids),
378
                }
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
379
            if self.version < (2, 17, 1):
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
380
                data.update({'format': 'rdf'})
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
381
            else:
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
382
                data.update({'ctype': 'rdf'})
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
383
            bug_tag = 'bz:bug'
384
            id_tag = 'bz:id'
385
            status_tag = 'bz:bug_status'
386
            resolution_tag = 'bz:resolution'
11304.1.12 by Bryce Harrington
Implement retrieving priority+severity from bugzilla into importance
387
            priority_tag = 'bz:priority'
11304.1.13 by Bryce Harrington
Looks like the bugzilla term is 'bug_severity'. Go consistency.
388
            severity_tag = 'bz:bug_severity'
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
389
12221.1.2 by Jeroen Vermeulen
Repost on redirect to Bugzilla's search page.
390
        buglist_xml = self._postPage(
391
            buglist_page, data, repost_on_redirect=True)
392
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
393
        try:
394
            document = self._parseDOMString(buglist_xml)
395
        except xml.parsers.expat.ExpatError, e:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
396
            raise UnparsableBugData(
397
                "Failed to parse XML description for %s bugs %s: %s"
398
                % (self.baseurl, bug_ids, e))
12221.1.4 by Jeroen Vermeulen
Check that bugzilla search result really looks like a search result page.
399
        self._checkBugSearchResult(document)
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
400
401
        bug_nodes = document.getElementsByTagName(bug_tag)
402
        for bug_node in bug_nodes:
403
            # We use manual iteration to pick up id_tags instead of
404
            # getElementsByTagName because the latter does a recursive
405
            # search, and in some documents we've found the id_tag to
406
            # appear under other elements (such as "has_duplicates") in
407
            # the document hierarchy.
408
            bug_id_nodes = [node for node in bug_node.childNodes if
409
                            node.nodeName == id_tag]
410
            if not bug_id_nodes:
411
                # Something in the output is really weird; this will
412
                # show up as a bug not found, but we can catch that
413
                # later in the error logs.
414
                continue
415
            bug_id_node = bug_id_nodes[0]
416
            assert len(bug_id_node.childNodes) == 1, (
417
                "id node should contain a non-empty text string.")
418
            bug_id = str(bug_id_node.childNodes[0].data)
419
            # This assertion comes in late so we can at least tell what
420
            # bug caused this crash.
421
            assert len(bug_id_nodes) == 1, ("Should be only one id node, "
422
                "but %s had %s." % (bug_id, len(bug_id_nodes)))
423
424
            status_nodes = bug_node.getElementsByTagName(status_tag)
425
            if not status_nodes:
426
                # Older versions of bugzilla used bz:status; this was
427
                # later changed to bz:bug_status. For robustness, and
428
                # because there is practically no risk of reading wrong
429
                # data here, just try the older format as well.
430
                status_nodes = bug_node.getElementsByTagName("bz:status")
431
            assert len(status_nodes) == 1, ("Couldn't find a status "
432
                                            "node for bug %s." % bug_id)
433
            bug_status_node = status_nodes[0]
434
            assert len(bug_status_node.childNodes) == 1, (
435
                "status node for bug %s should contain a non-empty "
436
                "text string." % bug_id)
437
            status = bug_status_node.childNodes[0].data
438
439
            resolution_nodes = bug_node.getElementsByTagName(resolution_tag)
440
            assert len(resolution_nodes) <= 1, (
441
                "Should be only one resolution node for bug %s." % bug_id)
442
            if resolution_nodes:
443
                assert len(resolution_nodes[0].childNodes) <= 1, (
444
                    "Resolution for bug %s should just contain "
445
                    "a string." % bug_id)
446
                if resolution_nodes[0].childNodes:
447
                    resolution = resolution_nodes[0].childNodes[0].data
448
                    status += ' %s' % resolution
449
            self.remote_bug_status[bug_id] = status
450
11304.1.12 by Bryce Harrington
Implement retrieving priority+severity from bugzilla into importance
451
            # Priority (for Importance)
452
            priority = ''
453
            priority_nodes = bug_node.getElementsByTagName(priority_tag)
454
            assert len(priority_nodes) <= 1, (
455
                "Should only be one priority node for bug %s" % bug_id)
456
            if priority_nodes:
457
                bug_priority_node = priority_nodes[0]
458
                assert len(bug_priority_node.childNodes) == 1, (
459
                    "priority node for bug %s should contain a non-empty "
460
                    "text string." % bug_id)
461
                priority = bug_priority_node.childNodes[0].data
462
463
            # Severity (for Importance)
11304.1.18 by Bryce Harrington
Further handling of IssueZilla's lack of severity field
464
            if severity_tag:
465
                severity_nodes = bug_node.getElementsByTagName(severity_tag)
466
                assert len(severity_nodes) <= 1, (
467
                    "Should only be one severity node for bug %s." % bug_id)
468
                if severity_nodes:
469
                    assert len(severity_nodes[0].childNodes) <= 1, (
470
                        "Severity for bug %s should just contain "
471
                        "a string." % bug_id)
472
                    if severity_nodes[0].childNodes:
473
                        severity = severity_nodes[0].childNodes[0].data
474
                        priority += ' %s' % severity
11304.1.12 by Bryce Harrington
Implement retrieving priority+severity from bugzilla into importance
475
            self.remote_bug_importance[bug_id] = priority
476
477
            # Product
7781.1.3 by Bjorn Tillenius
Implement Bugzilla.getRemoteProduct().
478
            product_nodes = bug_node.getElementsByTagName('bz:product')
7781.1.4 by Bjorn Tillenius
Handle the case where we don't get the product in the XML listing.
479
            assert len(product_nodes) <= 1, (
480
                "Should be at most one product node for bug %s." % bug_id)
481
            if len(product_nodes) == 0:
482
                self.remote_bug_product[bug_id] = None
483
            else:
484
                product_node = product_nodes[0]
485
                self.remote_bug_product[bug_id] = (
486
                    product_node.childNodes[0].data)
7781.1.3 by Bjorn Tillenius
Implement Bugzilla.getRemoteProduct().
487
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
488
    def getRemoteImportance(self, bug_id):
11304.1.1 by Bryce Harrington
Stub in a getRemoteImportance() call. Doesn't do much but tests pass.
489
        """See `ExternalBugTracker`."""
490
        try:
11304.1.2 by Bryce Harrington
Lookup importance locally
491
            if bug_id not in self.remote_bug_importance:
11304.1.1 by Bryce Harrington
Stub in a getRemoteImportance() call. Doesn't do much but tests pass.
492
                return "Bug %s is not in remote_bug_importance" %(bug_id)
493
            return self.remote_bug_importance[bug_id]
494
        except:
495
            return UNKNOWN_REMOTE_IMPORTANCE
5863.5.3 by Graham Binns
Moved bugzilla into its own module.
496
497
    def getRemoteStatus(self, bug_id):
498
        """See ExternalBugTracker."""
499
        if not bug_id.isdigit():
500
            raise InvalidBugId(
501
                "Bugzilla (%s) bug number not an integer: %s" % (
502
                    self.baseurl, bug_id))
503
        try:
504
            return self.remote_bug_status[bug_id]
505
        except KeyError:
506
            raise BugNotFound(bug_id)
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
507
7781.1.3 by Bjorn Tillenius
Implement Bugzilla.getRemoteProduct().
508
    def getRemoteProduct(self, remote_bug):
509
        """See `IExternalBugTracker`."""
7781.1.6 by Bjorn Tillenius
Make sure getRemoteProduct() raises BugNotFound.
510
        if remote_bug not in self.remote_bug_product:
511
            raise BugNotFound(remote_bug)
7781.1.3 by Bjorn Tillenius
Implement Bugzilla.getRemoteProduct().
512
        return self.remote_bug_product[remote_bug]
513
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
514
6532.1.15 by Graham Binns
Moved the decorator above the class it decorates.
515
def needs_authentication(func):
516
    """Decorator for automatically authenticating if needed.
517
518
    If an `xmlrpclib.Fault` with error code 410 is raised by the
519
    function, we'll try to authenticate and call the function again.
520
    """
11304.1.22 by Bryce Harrington
Cleanup a heapload of lint errors
521
6532.1.15 by Graham Binns
Moved the decorator above the class it decorates.
522
    def decorator(self, *args, **kwargs):
523
        try:
524
            return func(self, *args, **kwargs)
525
        except xmlrpclib.Fault, fault:
526
            # Catch authentication errors only.
527
            if fault.faultCode != 410:
528
                raise
11304.1.22 by Bryce Harrington
Cleanup a heapload of lint errors
529
6532.1.15 by Graham Binns
Moved the decorator above the class it decorates.
530
            self._authenticate()
531
            return func(self, *args, **kwargs)
532
    return decorator
533
534
9150.2.2 by Graham Binns
Add a BugzillaAPI ExternalBugTracker.
535
class BugzillaAPI(Bugzilla):
536
    """An `ExternalBugTracker` to handle Bugzillas that offer an API."""
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
537
9947.4.2 by Graham Binns
Added tests and implementation for BugzillaAPI's implementation of ISupportsBackLinking.
538
    implements(
539
        ISupportsBackLinking, ISupportsCommentImport, ISupportsCommentPushing)
9150.5.6 by Graham Binns
BugzillaAPI now implements ISupportsCommentImport.
540
6532.1.3 by Graham Binns
Added tests for _authenticate.
541
    def __init__(self, baseurl, xmlrpc_transport=None,
542
                 internal_xmlrpc_transport=None):
9150.2.2 by Graham Binns
Add a BugzillaAPI ExternalBugTracker.
543
        super(BugzillaAPI, self).__init__(baseurl)
6797.2.5 by Graham Binns
Made properties of BugzillaLPPlugin non-public.
544
        self._bugs = {}
545
        self._bug_aliases = {}
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
546
6604.1.15 by Tom Berger
use the same plugin instance for probing and for communicating later ; also, only establish the xmlrpc endpoint in the class that actually uses it
547
        self.xmlrpc_endpoint = urlappend(self.baseurl, 'xmlrpc.cgi')
548
6532.1.3 by Graham Binns
Added tests for _authenticate.
549
        self.internal_xmlrpc_transport = internal_xmlrpc_transport
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
550
        if xmlrpc_transport is None:
6604.1.2 by Tom Berger
use a new XMLRPC transport which uses urllib2, handles cookies and proxies
551
            self.xmlrpc_transport = UrlLib2Transport(self.xmlrpc_endpoint)
6290.4.8 by Graham Binns
Review changes for barry.
552
        else:
553
            self.xmlrpc_transport = xmlrpc_transport
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
554
10065.1.1 by Gavin Panella
BugzillaAPI and BugzillaLPPlugin.getExternalBugTrackerToUse() needs to just return self, rather than use the inherited version.
555
    def getExternalBugTrackerToUse(self):
556
        """The Bugzilla API has been chosen, so return self."""
557
        return self
558
6604.1.15 by Tom Berger
use the same plugin instance for probing and for communicating later ; also, only establish the xmlrpc endpoint in the class that actually uses it
559
    @property
560
    def xmlrpc_proxy(self):
561
        """Return an `xmlrpclib.ServerProxy` to self.xmlrpc_endpoint."""
562
        return xmlrpclib.ServerProxy(
563
            self.xmlrpc_endpoint, transport=self.xmlrpc_transport)
564
9150.2.3 by Graham Binns
Added authentication methods.
565
    @property
566
    def credentials(self):
567
        credentials_config = config['checkwatches.credentials']
9150.2.4 by Graham Binns
Review changes for jtv.
568
569
        # Extract the hostname from the current base url using urlparse.
9150.2.3 by Graham Binns
Added authentication methods.
570
        hostname = urlparse(self.baseurl)[1]
571
        try:
9150.2.5 by Graham Binns
Review changes for jtv.
572
            # XXX gmb 2009-08-19 bug=391131
573
            #     We shouldn't be using this here. Ideally we'd be able
574
            #     to get the credentials from the BugTracker object.
575
            #     If you find yourself adding credentials for, for
576
            #     example, www.password.username.pirateninjah4x0rz.org,
577
            #     think about fixing the above bug instead.
9150.2.3 by Graham Binns
Added authentication methods.
578
            username = credentials_config['%s.username' % hostname]
579
            password = credentials_config['%s.password' % hostname]
580
            return {'login': username, 'password': password}
581
        except KeyError:
582
            raise BugTrackerAuthenticationError(
583
                self.baseurl, "No credentials found.")
584
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
585
    @ensure_no_transaction
9150.2.3 by Graham Binns
Added authentication methods.
586
    def _authenticate(self):
587
        """Authenticate with the remote Bugzilla instance.
588
589
        The native Bugzilla API uses a standard (username, password)
590
        paradigm for authentication. If the username and password are
591
        correct, Bugzilla will send back a login cookie which we can use
592
        to re-authenticate with each subsequent method call.
593
        """
594
        try:
595
            self.xmlrpc_proxy.User.login(self.credentials)
596
        except xmlrpclib.Fault, fault:
597
            raise BugTrackerAuthenticationError(
598
                self.baseurl,
599
                "Fault %s: %s" % (fault.faultCode, fault.faultString))
9150.2.2 by Graham Binns
Add a BugzillaAPI ExternalBugTracker.
600
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
601
    def _storeBugs(self, remote_bugs):
602
        """Store remote bugs in the local `bugs` dict."""
603
        for remote_bug in remote_bugs:
604
            self._bugs[remote_bug['id']] = remote_bug
605
606
            # The bug_aliases dict is a mapping between aliases and bug
607
            # IDs. We use the aliases dict to look up the correct ID for
608
            # a bug. This allows us to reference a bug by either ID or
609
            # alias.
11707.1.1 by Graham Binns
Fixed bug 660873.
610
            if remote_bug.get('alias', '') != '':
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
611
                self._bug_aliases[remote_bug['alias']] = remote_bug['id']
612
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
613
    @ensure_no_transaction
9150.2.7 by Graham Binns
Implemented getting the current DB time from the remote server and converting it to UTC.
614
    def getCurrentDBTime(self):
615
        """See `IExternalBugTracker`."""
616
        time_dict = self.xmlrpc_proxy.Bugzilla.time()
617
618
        # The server's DB time is the one that we want to use. However,
9150.2.8 by Graham Binns
Huzzah. Timezones work and I don't have to kill myself.
619
        # this may not be in UTC, so we need to convert it. Since we
620
        # can't guarantee that the timezone data returned by the server
621
        # is sane, we work out the server's offset from UTC by looking
622
        # at the difference between the web_time and the web_time_utc
623
        # values.
10060.1.4 by Gavin Panella
Remove more xmlrpclib.DateTime hackery.
624
        server_web_datetime = time_dict['web_time']
625
        server_web_datetime_utc = time_dict['web_time_utc']
9150.2.8 by Graham Binns
Huzzah. Timezones work and I don't have to kill myself.
626
        server_utc_offset = server_web_datetime - server_web_datetime_utc
10060.1.4 by Gavin Panella
Remove more xmlrpclib.DateTime hackery.
627
        server_db_datetime = time_dict['db_time']
9150.2.8 by Graham Binns
Huzzah. Timezones work and I don't have to kill myself.
628
        server_utc_datetime = server_db_datetime - server_utc_offset
629
        return server_utc_datetime.replace(tzinfo=pytz.timezone('UTC'))
9150.2.7 by Graham Binns
Implemented getting the current DB time from the remote server and converting it to UTC.
630
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
631
    def _getActualBugId(self, bug_id):
632
        """Return the actual bug id for an alias or id."""
633
        # See if bug_id is actually an alias.
634
        actual_bug_id = self._bug_aliases.get(bug_id)
635
636
        # bug_id isn't an alias, so try turning it into an int and
637
        # looking the bug up by ID.
638
        if actual_bug_id is not None:
639
            return actual_bug_id
640
        else:
641
            try:
642
                actual_bug_id = int(bug_id)
643
            except ValueError:
644
                # If bug_id can't be int()'d then it's likely an alias
645
                # that doesn't exist, so raise BugNotFound.
646
                raise BugNotFound(bug_id)
647
648
            # Check that the bug does actually exist. That way we're
649
            # treating integer bug IDs and aliases in the same way.
650
            if actual_bug_id not in self._bugs:
651
                raise BugNotFound(bug_id)
652
653
            return actual_bug_id
654
655
    def _getBugIdsToRetrieve(self, bug_ids):
656
        """For a set of bug IDs, return those for which we have no data."""
657
        bug_ids_to_retrieve = []
658
        for bug_id in bug_ids:
659
            try:
10060.1.2 by Gavin Panella
Fix lint.
660
                self._getActualBugId(bug_id)
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
661
            except BugNotFound:
662
                bug_ids_to_retrieve.append(bug_id)
663
664
        return bug_ids_to_retrieve
665
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
666
    @ensure_no_transaction
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
667
    def initializeRemoteBugDB(self, bug_ids):
668
        """See `IExternalBugTracker`."""
669
        # First, discard all those bug IDs about which we already have
670
        # data.
671
        bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids)
672
673
        # Pull the bug data from the remote server. permissive=True here
674
        # prevents Bugzilla from erroring if we ask for a bug it doesn't
675
        # have.
676
        response_dict = self.xmlrpc_proxy.Bug.get({
677
            'ids': bug_ids_to_retrieve,
678
            'permissive': True,
679
            })
680
        remote_bugs = response_dict['bugs']
681
682
        self._storeBugs(remote_bugs)
683
9150.3.1 by Graham Binns
Refactored getRemoteStatuses() into BugzillaAPI.
684
    def getRemoteStatus(self, bug_id):
685
        """See `IExternalBugTracker`."""
686
        actual_bug_id = self._getActualBugId(bug_id)
687
688
        # Attempt to get the status and resolution from the bug. If
689
        # we don't have the data for either of them, raise an error.
690
        try:
691
            status = self._bugs[actual_bug_id]['status']
692
            resolution = self._bugs[actual_bug_id]['resolution']
10060.1.2 by Gavin Panella
Fix lint.
693
        except KeyError:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
694
            raise UnparsableBugData(
695
                "No status or resolution defined for bug %i" % (bug_id))
9150.3.1 by Graham Binns
Refactored getRemoteStatuses() into BugzillaAPI.
696
697
        if resolution != '':
698
            return "%s %s" % (status, resolution)
699
        else:
700
            return status
701
11304.1.5 by Bryce Harrington
Implement getRemoteImportance() for BugzillaAPI sub-class
702
    def getRemoteImportance(self, bug_id):
703
        """See `IExternalBugTracker`."""
704
        actual_bug_id = self._getActualBugId(bug_id)
705
706
        # Attempt to get the priority and severity from the bug.
707
        # If we don't have the data for either, raise an error.
708
        try:
709
            priority = self._bugs[actual_bug_id]['priority']
710
            severity = self._bugs[actual_bug_id]['severity']
711
        except KeyError:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
712
            raise UnparsableBugData(
713
                "No priority or severity defined for bug %i" % bug_id)
11304.1.5 by Bryce Harrington
Implement getRemoteImportance() for BugzillaAPI sub-class
714
715
        if severity != '':
716
            return "%s %s" % (priority, severity)
717
        else:
718
            return priority
719
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
720
    @ensure_no_transaction
9150.3.4 by Graham Binns
Added implementation of getModifiedRemoteBugs().
721
    def getModifiedRemoteBugs(self, bug_ids, last_checked):
722
        """See `IExternalBugTracker`."""
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
723
        response_dict = self.xmlrpc_proxy.Bug.search(
724
            {'id': bug_ids, 'last_change_time': last_checked})
9150.3.4 by Graham Binns
Added implementation of getModifiedRemoteBugs().
725
        remote_bugs = response_dict['bugs']
726
        # Store the bugs we've imported and return only their IDs.
727
        self._storeBugs(remote_bugs)
9245.2.1 by Graham Binns
BugzillaAPI.getModifiedRemoteBugs() now returns a list of strings, not ints.
728
        # Marshal the bug IDs into strings before returning them since
729
        # the remote Bugzilla may return ints rather than strings.
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
730
        return [str(remote_bug['id']) for remote_bug in remote_bugs]
9150.3.4 by Graham Binns
Added implementation of getModifiedRemoteBugs().
731
9150.3.6 by Graham Binns
Moved getRemoteProduct() to BugzillaAPI.
732
    def getRemoteProduct(self, remote_bug):
733
        """See `IExternalBugTracker`."""
734
        actual_bug_id = self._getActualBugId(remote_bug)
735
        return self._bugs[actual_bug_id]['product']
736
9150.3.7 by Graham Binns
Moved getProductsForRemoteBugs() into BugzillaAPI.
737
    def getProductsForRemoteBugs(self, bug_ids):
738
        """Return the products to which a set of remote bugs belong.
739
740
        :param bug_ids: A list of bug IDs or aliases.
741
        :returns: A dict of (bug_id_or_alias, product) mappings. If a
742
            bug ID specified in `bug_ids` is invalid, it will be ignored.
743
        """
744
        # Fetch from the server those bugs that we haven't already
745
        # fetched.
746
        self.initializeRemoteBugDB(bug_ids)
747
748
        bug_products = {}
749
        for bug_id in bug_ids:
750
            # If one of the bugs we're trying to get the product for
751
            # doesn't exist, just skip it.
752
            try:
753
                actual_bug_id = self._getActualBugId(bug_id)
754
            except BugNotFound:
755
                continue
756
757
            bug_dict = self._bugs[actual_bug_id]
758
            bug_products[bug_id] = bug_dict['product']
759
760
        return bug_products
761
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
762
    def getCommentIds(self, remote_bug_id):
9150.5.3 by Graham Binns
Added tests and implementation for getCommentIds().
763
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
764
        actual_bug_id = self._getActualBugId(remote_bug_id)
9150.5.3 by Graham Binns
Added tests and implementation for getCommentIds().
765
766
        # Check that the bug exists, first.
767
        if actual_bug_id not in self._bugs:
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
768
            raise BugNotFound(remote_bug_id)
9150.5.3 by Graham Binns
Added tests and implementation for getCommentIds().
769
770
        # Get only the remote comment IDs and store them in the
771
        # 'comments' field of the bug.
9302.1.1 by Graham Binns
Fixed bugs 422848 and 423046.
772
        return_dict = self.xmlrpc_proxy.Bug.comments({
9150.5.3 by Graham Binns
Added tests and implementation for getCommentIds().
773
            'ids': [actual_bug_id],
774
            'include_fields': ['id'],
775
            })
776
777
        # We need to convert bug and comment ids to strings (see bugs
778
        # 248662 amd 248938).
9302.1.1 by Graham Binns
Fixed bugs 422848 and 423046.
779
        bug_comments_dict = return_dict['bugs']
780
        bug_comments = bug_comments_dict[str(actual_bug_id)]['comments']
781
9150.5.3 by Graham Binns
Added tests and implementation for getCommentIds().
782
        return [str(comment['id']) for comment in bug_comments]
783
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
784
    @ensure_no_transaction
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
785
    def fetchComments(self, remote_bug_id, comment_ids):
9150.5.4 by Graham Binns
Added tests and implementation for fetchComments().
786
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
787
        actual_bug_id = self._getActualBugId(remote_bug_id)
9150.5.4 by Graham Binns
Added tests and implementation for fetchComments().
788
789
        # We need to cast comment_ids to integers, since
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
790
        # CheckwatchesMaster.importBugComments() will pass us a list of
9150.5.4 by Graham Binns
Added tests and implementation for fetchComments().
791
        # strings (see bug 248938).
792
        comment_ids = [int(comment_id) for comment_id in comment_ids]
793
794
        # Fetch the comments we want.
795
        return_dict = self.xmlrpc_proxy.Bug.comments({
796
            'comment_ids': comment_ids,
797
            })
798
        comments = return_dict['comments']
799
800
        # As a sanity check, drop any comments that don't belong to the
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
801
        # bug in remote_bug_id.
9150.5.4 by Graham Binns
Added tests and implementation for fetchComments().
802
        for comment_id, comment in comments.items():
803
            if int(comment['bug_id']) != actual_bug_id:
804
                del comments[comment_id]
805
9302.1.1 by Graham Binns
Fixed bugs 422848 and 423046.
806
        # Ensure that comment IDs are converted to ints.
807
        comments_with_int_ids = dict(
808
            (int(id), comments[id]) for id in comments)
809
        self._bugs[actual_bug_id]['comments'] = comments_with_int_ids
9150.5.4 by Graham Binns
Added tests and implementation for fetchComments().
810
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
811
    def getPosterForComment(self, remote_bug_id, comment_id):
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
812
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
813
        actual_bug_id = self._getActualBugId(remote_bug_id)
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
814
815
        # We need to cast comment_id to integers, since
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
816
        # CheckwatchesMaster.importBugComments() will pass us a string (see
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
817
        # bug 248938).
818
        comment_id = int(comment_id)
819
820
        comment = self._bugs[actual_bug_id]['comments'][comment_id]
821
        display_name, email = parseaddr(comment['author'])
822
823
        # If the name is empty then we return None so that
824
        # IPersonSet.ensurePerson() can actually do something with it.
825
        if not display_name:
826
            display_name = None
827
828
        return (display_name, email)
829
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
830
    def getMessageForComment(self, remote_bug_id, comment_id, poster):
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
831
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
832
        actual_bug_id = self._getActualBugId(remote_bug_id)
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
833
834
        # We need to cast comment_id to integers, since
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
835
        # CheckwatchesMaster.importBugComments() will pass us a string (see
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
836
        # bug 248938).
837
        comment_id = int(comment_id)
838
        comment = self._bugs[actual_bug_id]['comments'][comment_id]
10060.1.4 by Gavin Panella
Remove more xmlrpclib.DateTime hackery.
839
        return getUtility(IMessageSet).fromText(
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
840
            owner=poster, subject='', content=comment['text'],
10060.1.4 by Gavin Panella
Remove more xmlrpclib.DateTime hackery.
841
            datecreated=comment['time'].replace(tzinfo=pytz.timezone('UTC')))
9150.5.5 by Graham Binns
Refactored getPoster* and getMessageForComment() into BugzillaAPI.
842
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
843
    @ensure_no_transaction
9234.2.2 by Graham Binns
BugzillaAPI now implements ISupportsCommentPushing.
844
    @needs_authentication
845
    def addRemoteComment(self, remote_bug, comment_body, rfc822msgid):
846
        """Add a comment to the remote bugtracker.
847
848
        See `ISupportsCommentPushing`.
849
        """
850
        actual_bug_id = self._getActualBugId(remote_bug)
851
852
        request_params = {
853
            'id': actual_bug_id,
854
            'comment': comment_body,
855
            }
856
        return_dict = self.xmlrpc_proxy.Bug.add_comment(request_params)
857
858
        # We cast the return value to string, since that's what
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
859
        # CheckwatchesMaster will expect (see bug 248938).
9234.2.2 by Graham Binns
BugzillaAPI now implements ISupportsCommentPushing.
860
        return str(return_dict['id'])
861
9947.4.2 by Graham Binns
Added tests and implementation for BugzillaAPI's implementation of ISupportsBackLinking.
862
    def getLaunchpadBugId(self, remote_bug):
863
        """Return the Launchpad bug ID for the remote bug.
864
865
        See `ISupportsBackLinking`.
866
        """
9947.4.3 by Graham Binns
Added XXX for bug 490267 at Abel's request.
867
        # XXX gmb 2009-11-30 bug=490267
868
        #     In fact, this method always returns None due to bug
869
        #     490267. Once the bug is fixed in Bugzilla we should update
870
        #     this method.
9947.4.2 by Graham Binns
Added tests and implementation for BugzillaAPI's implementation of ISupportsBackLinking.
871
        return None
872
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
873
    @ensure_no_transaction
9947.4.2 by Graham Binns
Added tests and implementation for BugzillaAPI's implementation of ISupportsBackLinking.
874
    @needs_authentication
10694.2.24 by Gavin Panella
Change the function signature style as suggested in review.
875
    def setLaunchpadBugId(self, remote_bug, launchpad_bug_id,
876
                          launchpad_bug_url):
9947.4.2 by Graham Binns
Added tests and implementation for BugzillaAPI's implementation of ISupportsBackLinking.
877
        """Set the Launchpad bug for a given remote bug.
878
879
        See `ISupportsBackLinking`.
880
        """
881
        actual_bug_id = self._getActualBugId(remote_bug)
882
883
        request_params = {
884
            'ids': [actual_bug_id],
885
            'add': [launchpad_bug_url],
886
            }
887
888
        self.xmlrpc_proxy.Bug.update_see_also(request_params)
889
9150.2.2 by Graham Binns
Add a BugzillaAPI ExternalBugTracker.
890
891
class BugzillaLPPlugin(BugzillaAPI):
892
    """An `ExternalBugTracker` to handle Bugzillas using the LP Plugin."""
893
894
    implements(
895
        ISupportsBackLinking, ISupportsCommentImport,
896
        ISupportsCommentPushing)
897
10065.1.1 by Gavin Panella
BugzillaAPI and BugzillaLPPlugin.getExternalBugTrackerToUse() needs to just return self, rather than use the inherited version.
898
    def getExternalBugTrackerToUse(self):
899
        """The Bugzilla LP Plugin has been chosen, so return self."""
900
        return self
901
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
902
    @ensure_no_transaction
6532.1.3 by Graham Binns
Added tests for _authenticate.
903
    def _authenticate(self):
6532.1.18 by Graham Binns
Review changes for bac.
904
        """Authenticate with the remote Bugzilla instance.
905
906
        Authentication works by means of using a LoginToken of type
907
        BUGTRACKER. We send the token text to the remote server as a
908
        parameter to Launchpad.login(), which verifies it using the
909
        standard launchpad.net/token/$token/+bugtracker-handshake URL.
910
911
        If the token is valid, Bugzilla will send us a user ID as a
912
        return value for the call to Launchpad.login() and will set two
913
        cookies in the response header, Bugzilla_login and
914
        Bugzilla_logincookie, which we can then use to re-authenticate
915
        ourselves for each subsequent method call.
916
        """
6532.1.3 by Graham Binns
Added tests for _authenticate.
917
        internal_xmlrpc_server = xmlrpclib.ServerProxy(
918
            config.checkwatches.xmlrpc_url,
919
            transport=self.internal_xmlrpc_transport)
920
921
        token_text = internal_xmlrpc_server.newBugTrackerToken()
922
7130.2.8 by Graham Binns
BugzillaLPPlugin._authenticate() now raises a BugTrackerAuthenticationError when an error occurs during authentication.
923
        try:
10060.1.2 by Gavin Panella
Fix lint.
924
            self.xmlrpc_proxy.Launchpad.login(
7130.2.8 by Graham Binns
BugzillaLPPlugin._authenticate() now raises a BugTrackerAuthenticationError when an error occurs during authentication.
925
                {'token': token_text})
926
        except xmlrpclib.Fault, fault:
927
            message = 'XML-RPC Fault: %s "%s"' % (
928
                fault.faultCode, fault.faultString)
929
            raise BugTrackerAuthenticationError(
930
                self.baseurl, message)
931
        except xmlrpclib.ProtocolError, error:
932
            message = 'Protocol error: %s "%s"' % (
933
                error.errcode, error.errmsg)
934
            raise BugTrackerAuthenticationError(
935
                self.baseurl, message)
6532.1.9 by Graham Binns
Tests seem to work now.
936
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
937
    @ensure_no_transaction
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
938
    def getModifiedRemoteBugs(self, bug_ids, last_checked):
6797.2.2 by Graham Binns
Review changes for Barry.
939
        """See `IExternalBugTracker`."""
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
940
        # We pass permissive=True to ensure that Bugzilla won't error
941
        # if we ask for a bug that doesn't exist.
942
        response_dict = self.xmlrpc_proxy.Launchpad.get_bugs({
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
943
            'ids': bug_ids,
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
944
            'changed_since': last_checked,
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
945
            'permissive': True,
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
946
            })
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
947
        remote_bugs = response_dict['bugs']
948
        # Store the bugs we've imported and return only their IDs.
949
        self._storeBugs(remote_bugs)
10060.1.1 by Gavin Panella
Force the use of datetime in the XML-RPC transport, and remove the workarounds in BugzillaAPI and BugzillaLPPlugin.
950
        return [remote_bug['id'] for remote_bug in remote_bugs]
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
951
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
952
    @ensure_no_transaction
7147.1.3 by Graham Binns
You can now pass a products list to BugzillaLPPlugin.initializeRemoteBugDB().
953
    def initializeRemoteBugDB(self, bug_ids, products=None):
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
954
        """See `IExternalBugTracker`."""
955
        # First, discard all those bug IDs about which we already have
956
        # data.
9150.2.9 by Graham Binns
Added initializeRemoteBugDB() to BugzillaAPI and refactored some utility functions out of BugzillaLPPlugin.
957
        bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids)
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
958
959
        # Next, grab the bugs we still need from the remote server.
6797.2.2 by Graham Binns
Review changes for Barry.
960
        # We pass permissive=True to ensure that Bugzilla won't error if
961
        # we ask for a bug that doesn't exist.
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
962
        request_args = {
963
            'ids': bug_ids_to_retrieve,
964
            'permissive': True,
965
            }
7147.1.3 by Graham Binns
You can now pass a products list to BugzillaLPPlugin.initializeRemoteBugDB().
966
967
        if products is not None:
968
            request_args['products'] = products
969
6759.2.3 by Graham Binns
Added implementation to covert bug 203559.
970
        response_dict = self.xmlrpc_proxy.Launchpad.get_bugs(request_args)
971
        remote_bugs = response_dict['bugs']
972
973
        self._storeBugs(remote_bugs)
974
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
975
    @ensure_no_transaction
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
976
    def getCurrentDBTime(self):
977
        """See `IExternalBugTracker`."""
6570.1.2 by Graham Binns
Altered naming in BugzillaLPPlugin and merged parent branch.
978
        time_dict = self.xmlrpc_proxy.Launchpad.time()
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
979
980
        # Return the UTC time sent by the server so that we don't have
981
        # to care about timezones.
10060.1.4 by Gavin Panella
Remove more xmlrpclib.DateTime hackery.
982
        server_utc_time = time_dict['utc_time']
6290.4.4 by Graham Binns
Added tests and implementation for getCurrentDBTime().
983
        return server_utc_time.replace(tzinfo=pytz.timezone('UTC'))
984
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
985
    @ensure_no_transaction
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
986
    def getCommentIds(self, remote_bug_id):
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
987
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
988
        actual_bug_id = self._getActualBugId(remote_bug_id)
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
989
990
        # Check that the bug exists, first.
6797.2.5 by Graham Binns
Made properties of BugzillaLPPlugin non-public.
991
        if actual_bug_id not in self._bugs:
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
992
            raise BugNotFound(remote_bug_id)
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
993
994
        # Get only the remote comment IDs and store them in the
995
        # 'comments' field of the bug.
996
        request_params = {
997
            'bug_ids': [actual_bug_id],
6671.1.5 by Graham Binns
Altered the method signature of Launchpad.comment().
998
            'include_fields': ['id'],
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
999
            }
6671.1.6 by Graham Binns
Removed lint.
1000
        bug_comments_dict = self.xmlrpc_proxy.Launchpad.comments(
1001
            request_params)
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
1002
6706.1.1 by Graham Binns
Added tests and fix for bug 248662.
1003
        # We need to convert actual_bug_id to a string due to a quirk
1004
        # with XML-RPC (see bug 248662).
6706.1.2 by Graham Binns
Change suggested by bac.
1005
        bug_comments = bug_comments_dict['bugs'][str(actual_bug_id)]
6712.1.1 by Graham Binns
Fixed bug 248938.
1006
1007
        # We also need to convert each comment ID to a string, since
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
1008
        # that's what CheckwatchesMaster.importBugComments() expects (see
6712.1.1 by Graham Binns
Fixed bug 248938.
1009
        # bug 248938).
1010
        return [str(comment['id']) for comment in bug_comments]
6506.5.6 by Graham Binns
Added tests and implementation for fetchComments(). Made getCommentIds() work correctly.
1011
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
1012
    @ensure_no_transaction
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
1013
    def fetchComments(self, remote_bug_id, comment_ids):
6506.5.6 by Graham Binns
Added tests and implementation for fetchComments(). Made getCommentIds() work correctly.
1014
        """See `ISupportsCommentImport`."""
10122.2.1 by Gavin Panella
Don't pass BugWatch objects (i.e. a db/model object) into code in the externalbugtracker module.
1015
        actual_bug_id = self._getActualBugId(remote_bug_id)
6506.5.6 by Graham Binns
Added tests and implementation for fetchComments(). Made getCommentIds() work correctly.
1016
6712.1.1 by Graham Binns
Fixed bug 248938.
1017
        # We need to cast comment_ids to integers, since
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
1018
        # CheckwatchesMaster.importBugComments() will pass us a list of
6712.1.1 by Graham Binns
Fixed bug 248938.
1019
        # strings (see bug 248938).
1020
        comment_ids = [int(comment_id) for comment_id in comment_ids]
1021
6506.5.6 by Graham Binns
Added tests and implementation for fetchComments(). Made getCommentIds() work correctly.
1022
        # Fetch the comments we want.
1023
        request_params = {
1024
            'bug_ids': [actual_bug_id],
1025
            'ids': comment_ids,
1026
            }
6671.1.6 by Graham Binns
Removed lint.
1027
        bug_comments_dict = self.xmlrpc_proxy.Launchpad.comments(
1028
            request_params)
6706.1.1 by Graham Binns
Added tests and fix for bug 248662.
1029
1030
        # We need to convert actual_bug_id to a string here due to a
1031
        # quirk with XML-RPC (see bug 248662).
1032
        comment_list = bug_comments_dict['bugs'][str(actual_bug_id)]
6506.5.8 by Graham Binns
Added tests and implementation for getMessageForComment().
1033
1034
        # Transfer the comment list into a dict.
6506.5.10 by Graham Binns
Review changes for Gavin.
1035
        bug_comments = dict(
1036
            (comment['id'], comment) for comment in comment_list)
6506.5.6 by Graham Binns
Added tests and implementation for fetchComments(). Made getCommentIds() work correctly.
1037
6797.2.5 by Graham Binns
Made properties of BugzillaLPPlugin non-public.
1038
        self._bugs[actual_bug_id]['comments'] = bug_comments
6506.5.5 by Graham Binns
Added tests and implementation for getCommentIds().
1039
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
1040
    @ensure_no_transaction
6527.10.8 by Graham Binns
Added an ordered_dict_by_string() method.
1041
    @needs_authentication
1042
    def addRemoteComment(self, remote_bug, comment_body, rfc822msgid):
1043
        """Add a comment to the remote bugtracker.
1044
1045
        See `ISupportsCommentPushing`.
1046
        """
6527.10.9 by Graham Binns
Added a test to ensure that the comment is pushed properly to the remote bugtracker.
1047
        actual_bug_id = self._getActualBugId(remote_bug)
1048
6527.10.8 by Graham Binns
Added an ordered_dict_by_string() method.
1049
        request_params = {
6527.10.9 by Graham Binns
Added a test to ensure that the comment is pushed properly to the remote bugtracker.
1050
            'id': actual_bug_id,
6527.10.8 by Graham Binns
Added an ordered_dict_by_string() method.
1051
            'comment': comment_body,
1052
            }
6671.1.3 by Graham Binns
Updated Bugzilla[LPPlugin] to point to the right namespace.
1053
        return_dict = self.xmlrpc_proxy.Launchpad.add_comment(request_params)
6527.10.8 by Graham Binns
Added an ordered_dict_by_string() method.
1054
6712.1.1 by Graham Binns
Fixed bug 248938.
1055
        # We cast the return value to string, since that's what
10694.2.25 by Graham Binns
Renamed BugWatchUpdater -> CheckwatchesMaster. This is the wrong name for it, but I did it to avoid bikeshedding.
1056
        # CheckwatchesMaster will expect (see bug 248938).
6712.1.1 by Graham Binns
Fixed bug 248938.
1057
        return str(return_dict['comment_id'])
7026.2.2 by Graham Binns
Added implementation for getLaunchpadBugId() and setLaunchpadBugId().
1058
1059
    def getLaunchpadBugId(self, remote_bug):
1060
        """Return the current Launchpad bug ID for a given remote bug.
1061
1062
        See `ISupportsBackLinking`.
1063
        """
1064
        actual_bug_id = self._getActualBugId(remote_bug)
1065
1066
        # Grab the internals dict from the bug, if there is one. If
1067
        # there isn't, return None, since there's no Launchpad bug ID to
1068
        # be had.
1069
        internals = self._bugs[actual_bug_id].get('internals', None)
1070
        if internals is None:
1071
            return None
1072
1073
        # Extract the Launchpad bug ID and return it. Return None if
1074
        # there isn't one or it's set to an empty string.
1075
        launchpad_bug_id = internals.get('launchpad_id', None)
7026.2.4 by Graham Binns
Review changes for Celso.
1076
        if launchpad_bug_id == '':
1077
            launchpad_bug_id = None
1078
1079
        return launchpad_bug_id
7026.2.2 by Graham Binns
Added implementation for getLaunchpadBugId() and setLaunchpadBugId().
1080
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
1081
    @ensure_no_transaction
7026.2.2 by Graham Binns
Added implementation for getLaunchpadBugId() and setLaunchpadBugId().
1082
    @needs_authentication
10694.2.24 by Gavin Panella
Change the function signature style as suggested in review.
1083
    def setLaunchpadBugId(self, remote_bug, launchpad_bug_id,
1084
                          launchpad_bug_url):
7026.2.2 by Graham Binns
Added implementation for getLaunchpadBugId() and setLaunchpadBugId().
1085
        """Set the Launchpad bug for a given remote bug.
1086
1087
        See `ISupportsBackLinking`.
1088
        """
1089
        actual_bug_id = self._getActualBugId(remote_bug)
1090
1091
        request_params = {
1092
            'id': actual_bug_id,
1093
            'launchpad_id': launchpad_bug_id,
1094
            }
1095
1096
        self.xmlrpc_proxy.Launchpad.set_link(request_params)