~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).
5897.3.1 by Graham Binns
Moved mantis into its own module.
3
4
"""Mantis ExternalBugTracker utility."""
5
6
__metaclass__ = type
5897.3.2 by Graham Binns
Fixed tests.
7
__all__ = ['Mantis', 'MantisLoginHandler']
5897.3.1 by Graham Binns
Moved mantis into its own module.
8
9
import cgi
10
import csv
12505.1.1 by Tim Penhey
Bring back in the mantis fix.
11
import logging
5897.3.1 by Graham Binns
Moved mantis into its own module.
12
import urllib
6061.2.59 by Gary Poster
remove use of ClientCookie
13
import urllib2
5897.3.1 by Graham Binns
Moved mantis into its own module.
14
from urlparse import urlunparse
15
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
16
from BeautifulSoup import (
17
    BeautifulSoup,
18
    Comment,
19
    SoupStrainer,
20
    )
21
8523.3.8 by Gavin Panella
Fix some imports in externalbugtracker.
22
from lp.bugs.externalbugtracker import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
23
    BugNotFound,
24
    BugTrackerConnectError,
25
    BugWatchUpdateError,
26
    ExternalBugTracker,
27
    InvalidBugId,
28
    LookupTree,
29
    UnknownRemoteStatusError,
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
30
    UnparsableBugData,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
31
    )
32
from lp.bugs.interfaces.bugtask import (
33
    BugTaskImportance,
34
    BugTaskStatus,
35
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
36
from lp.bugs.interfaces.externalbugtracker import UNKNOWN_REMOTE_IMPORTANCE
12579.1.1 by William Grant
Move lp.bugs.externalbugtracker.isolation to lp.services.database. It's not Bugs-specific.
37
from lp.services.database.isolation import ensure_no_transaction
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
38
from lp.services.propertycache import cachedproperty
14612.2.1 by William Grant
format-imports on lib/. So many imports.
39
from lp.services.webapp.url import urlparse
10573.1.1 by Abel Deuring
fix for bug 261254: Launchpad couldn't connect to ALSA Bug Tracker.
40
41
6061.2.59 by Gary Poster
remove use of ClientCookie
42
class MantisLoginHandler(urllib2.HTTPRedirectHandler):
43
    """Handler for urllib2.build_opener to automatically log-in
5897.3.1 by Graham Binns
Moved mantis into its own module.
44
    to Mantis anonymously if needed.
45
46
    The ALSA bug tracker is the only tested Mantis installation that
47
    actually needs this. For ALSA bugs, the dance is like so:
48
49
      1. We request bug 3301 ('jack sensing problem'):
50
           https://bugtrack.alsa-project.org/alsa-bug/view.php?id=3301
51
52
      2. Mantis redirects us to:
12225.6.1 by Gavin Panella
Update doctest to rST and fix some overlong lines in mantis.py.
53
           .../alsa-bug/login_page.php?
54
                 return=%2Falsa-bug%2Fview.php%3Fid%3D3301
5897.3.1 by Graham Binns
Moved mantis into its own module.
55
56
      3. We notice this, rewrite the query, and skip to login.php:
12225.6.1 by Gavin Panella
Update doctest to rST and fix some overlong lines in mantis.py.
57
           .../alsa-bug/login.php?
58
                 return=%2Falsa-bug%2Fview.php%3Fid%3D3301&
59
                 username=guest&password=guest
5897.3.1 by Graham Binns
Moved mantis into its own module.
60
61
      4. Mantis accepts our credentials then redirects us to the bug
62
         view page via a cookie test page (login_cookie_test.php)
63
    """
64
65
    def rewrite_url(self, url):
66
        scheme, host, path, params, query, fragment = urlparse(url)
67
68
        # If we can, skip the login page and submit credentials
69
        # directly. The query should contain a 'return' parameter
70
        # which, if our credentials are accepted, means we'll be
71
        # redirected back from whence we came. In other words, we'll
72
        # end up back at the bug page we first requested.
73
        login_page = '/login_page.php'
74
        if path.endswith(login_page):
75
            path = path[:-len(login_page)] + '/login.php'
76
            query = cgi.parse_qs(query, True)
77
            query['username'] = query['password'] = ['guest']
78
            if 'return' not in query:
12225.5.2 by Gavin Panella
Raise a BugTrackerConnectError in MantisLoginHandler instead of raising BugWatchUpdateWarning.
79
                raise BugTrackerConnectError(
80
                    url, ("Mantis redirected us to the login page "
81
                          "but did not set a return path."))
5897.3.1 by Graham Binns
Moved mantis into its own module.
82
83
            query = urllib.urlencode(query, True)
84
            url = urlunparse(
85
                (scheme, host, path, params, query, fragment))
86
12225.7.1 by Gavin Panella
Clean up some XXXs.
87
        # Previous versions of the Mantis external bug tracker fetched
88
        # login_anon.php in addition to the login.php method above, but none
89
        # of the Mantis installations tested actually needed this. For
90
        # example, the ALSA bugtracker actually issues an error "Your account
91
        # may be disabled" when accessing this page. For now it's better to
92
        # *not* try this page because we may end up annoying admins with
93
        # spurious login attempts.
5897.3.1 by Graham Binns
Moved mantis into its own module.
94
95
        return url
96
10573.1.1 by Abel Deuring
fix for bug 261254: Launchpad couldn't connect to ALSA Bug Tracker.
97
    def redirect_request(self, request, fp, code, msg, hdrs, new_url):
98
        return urllib2.HTTPRedirectHandler.redirect_request(
99
            self, request, fp, code, msg, hdrs, self.rewrite_url(new_url))
100
5897.3.1 by Graham Binns
Moved mantis into its own module.
101
12505.1.1 by Tim Penhey
Bring back in the mantis fix.
102
class MantisBugBatchParser:
103
    """A class that parses the batch of bug data.
104
105
    Using the CSV reader is pretty much essential since the data that comes
106
    back can include title text which can in turn contain field separators.
107
    You don't want to handle the unquoting yourself.
108
    """
109
110
    def __init__(self, csv_data, logger):
111
        # Clean out stray, unquoted newlines inside csv_data to avoid the CSV
112
        # module blowing up.  IDEA: perhaps if the size of csv_data is large
113
        # in the future, this could be moved into a generator.
114
        csv_data = [s.replace("\r", "") for s in csv_data]
115
        csv_data = [s.replace("\n", "") for s in csv_data]
116
        self.reader = csv.reader(csv_data)
117
        self.logger = logger
118
119
    def processCSVBugLine(self, bug_line, headers):
120
        """Processes a single line of the CSV."""
121
        bug = {}
122
        for index, header in enumerate(headers):
123
            try:
124
                data = bug_line[index]
125
            except IndexError:
126
                self.logger.warning("Line %r incomplete." % bug_line)
127
                return None
128
            bug[header] = data
129
        try:
130
            bug['id'] = int(bug['id'])
131
        except ValueError:
132
            self.logger.warning("Encountered invalid bug ID: %r." % bug['id'])
133
            return None
134
        return bug
135
136
    def parseHeaderLine(self, reader):
137
        # The first line of the CSV file is the header. We need to read
138
        # it because different Mantis instances have different header
139
        # ordering and even different columns in the export.
140
        try:
141
            headers = [h.lower() for h in reader.next()]
142
        except StopIteration:
143
            raise UnparsableBugData("Missing header line")
144
        missing_headers = [
145
            name for name in ('id', 'status', 'resolution')
146
            if name not in headers]
147
        if missing_headers:
148
            raise UnparsableBugData(
149
                "CSV header %r missing fields: %r" % (
150
                    headers, missing_headers))
151
        return headers
152
153
    def getBugs(self):
154
        headers = self.parseHeaderLine(self.reader)
155
        bugs = {}
156
        try:
157
            for bug_line in self.reader:
158
                bug = self.processCSVBugLine(bug_line, headers)
159
                if bug is not None:
160
                    bugs[bug['id']] = bug
161
            return bugs
162
        except csv.Error, error:
163
            raise UnparsableBugData("Exception parsing CSV file: %s." % error)
164
165
5897.3.1 by Graham Binns
Moved mantis into its own module.
166
class Mantis(ExternalBugTracker):
167
    """An `ExternalBugTracker` for dealing with Mantis instances.
168
169
    For a list of tested Mantis instances and their behaviour when
8971.25.1 by Gavin Panella
Update wiki links for pages moved to the development wiki.
170
    exported from, see:
171
172
        https://dev.launchpad.net/Bugs/ExternalBugTrackers/Mantis
5897.3.1 by Graham Binns
Moved mantis into its own module.
173
    """
174
10573.1.1 by Abel Deuring
fix for bug 261254: Launchpad couldn't connect to ALSA Bug Tracker.
175
    def __init__(self, baseurl):
176
        super(Mantis, self).__init__(baseurl)
177
        # Custom cookie aware opener that automatically sends anonymous
178
        # credentials to Mantis if (and only if) needed.
179
        self._cookie_handler = urllib2.HTTPCookieProcessor()
180
        self._opener = urllib2.build_opener(
181
            self._cookie_handler, MantisLoginHandler())
12505.1.1 by Tim Penhey
Bring back in the mantis fix.
182
        self._logger = logging.getLogger()
5897.3.1 by Graham Binns
Moved mantis into its own module.
183
10512.4.3 by Gavin Panella
Sprinkle ensure_no_transaction() in good places.
184
    @ensure_no_transaction
5897.3.1 by Graham Binns
Moved mantis into its own module.
185
    def urlopen(self, request, data=None):
6061.2.59 by Gary Poster
remove use of ClientCookie
186
        # We use urllib2 to make following cookies transparent.
5897.3.1 by Graham Binns
Moved mantis into its own module.
187
        # This is required for certain bugtrackers that require
188
        # cookies that actually do anything (as is the case with
189
        # Mantis). It's basically a drop-in replacement for
190
        # urllib2.urlopen() that tracks cookies. We also have a
6061.2.59 by Gary Poster
remove use of ClientCookie
191
        # customised urllib2 opener to handle transparent
5897.3.1 by Graham Binns
Moved mantis into its own module.
192
        # authentication.
193
        return self._opener.open(request, data)
194
195
    @cachedproperty
196
    def csv_data(self):
197
        """Attempt to retrieve a CSV export from the remote server.
198
199
        If the export fails (i.e. the response is 0-length), None will
200
        be returned.
201
        """
10573.1.1 by Abel Deuring
fix for bug 261254: Launchpad couldn't connect to ALSA Bug Tracker.
202
        return self._csv_data()
203
204
    def _csv_data(self):
205
        """See `csv_data()."""
5897.3.1 by Graham Binns
Moved mantis into its own module.
206
        # Next step is getting our query filter cookie set up; we need
207
        # to do this weird submit in order to get the closed bugs
208
        # included in the results; the default Mantis filter excludes
209
        # them. It's unlikely that all these parameters are actually
210
        # necessary, but it's easy to prepare the complete set from a
211
        # view_all_bugs.php form dump so let's keep it complete.
212
        data = {
213
           'type': '1',
214
           'page_number': '1',
215
           'view_type': 'simple',
216
           'reporter_id[]': '0',
217
           'user_monitor[]': '0',
218
           'handler_id[]': '0',
219
           'show_category[]': '0',
220
           'show_severity[]': '0',
221
           'show_resolution[]': '0',
222
           'show_profile[]': '0',
223
           'show_status[]': '0',
224
           # Some of the more modern Mantis trackers use
225
           # a value of 'hide_status[]': '-2' here but it appears that
226
           # [none] works. Oops, older Mantis uses 'none' here. Gross!
227
           'hide_status[]': '[none]',
228
           'show_build[]': '0',
229
           'show_version[]': '0',
230
           'fixed_in_version[]': '0',
231
           'show_priority[]': '0',
232
           'per_page': '50',
233
           'view_state': '0',
234
           'sticky_issues': 'on',
235
           'highlight_changed': '6',
236
           'relationship_type': '-1',
237
           'relationship_bug': '0',
238
           # Hack around the fact that the sorting parameter has
239
           # changed over time.
240
           'sort': 'last_updated',
241
           'sort_0': 'last_updated',
242
           'dir': 'DESC',
243
           'dir_0': 'DESC',
244
           'search': '',
245
           'filter': 'Apply Filter',
246
        }
12505.1.1 by Tim Penhey
Bring back in the mantis fix.
247
        try:
248
            self._postPage("view_all_set.php?f=3", data)
249
        except BugTrackerConnectError:
250
            return None
5897.3.1 by Graham Binns
Moved mantis into its own module.
251
252
        # Finally grab the full CSV export, which uses the
253
        # MANTIS_VIEW_ALL_COOKIE set in the previous step to specify
254
        # what's being viewed.
10573.1.1 by Abel Deuring
fix for bug 261254: Launchpad couldn't connect to ALSA Bug Tracker.
255
        try:
256
            csv_data = self._getPage("csv_export.php")
257
        except BugTrackerConnectError, value:
258
            # Some Mantis installations simply return a 500 error
259
            # when the csv_export.php page is accessed. Since the
260
            # bug data may be nevertheless available from ordinary
261
            # web pages, we simply ignore this error.
262
            if value.error.startswith('HTTP Error 500'):
263
                return None
264
            raise
5897.3.1 by Graham Binns
Moved mantis into its own module.
265
266
        if not csv_data:
267
            return None
268
        else:
269
            return csv_data
270
271
    def canUseCSVExports(self):
272
        """Return True if a Mantis instance supports CSV exports.
273
274
        If the Mantis instance cannot or does not support CSV exports,
275
        False will be returned.
276
        """
277
        return self.csv_data is not None
278
279
    def initializeRemoteBugDB(self, bug_ids):
280
        """See `ExternalBugTracker`.
281
282
        This method is overridden so that it can take into account the
283
        fact that not all Mantis instances support CSV exports. In
284
        those cases all bugs will be imported individually, regardless
285
        of how many there are.
286
        """
287
        self.bugs = {}
288
289
        if (len(bug_ids) > self.batch_query_threshold and
290
            self.canUseCSVExports()):
291
            # We only query for batches of bugs if the remote Mantis
292
            # instance supports CSV exports, otherwise we default to
293
            # screen-scraping on a per bug basis regardless of how many bugs
294
            # there are to retrieve.
295
            self.bugs = self.getRemoteBugBatch(bug_ids)
296
        else:
297
            for bug_id in bug_ids:
298
                bug_id, remote_bug = self.getRemoteBug(bug_id)
299
300
                if bug_id is not None:
301
                    self.bugs[bug_id] = remote_bug
302
303
    def getRemoteBug(self, bug_id):
304
        """See `ExternalBugTracker`."""
305
        # Only parse tables to save time and memory. If we didn't have
306
        # to check for application errors in the page (using
307
        # _checkForApplicationError) then we could be much more
308
        # specific than this.
309
        bug_page = BeautifulSoup(
310
            self._getPage('view.php?id=%s' % bug_id),
311
            convertEntities=BeautifulSoup.HTML_ENTITIES,
312
            parseOnlyThese=SoupStrainer('table'))
313
314
        app_error = self._checkForApplicationError(bug_page)
315
        if app_error:
316
            app_error_code, app_error_message = app_error
317
            # 1100 is ERROR_BUG_NOT_FOUND in Mantis (see
318
            # mantisbt/core/constant_inc.php).
319
            if app_error_code == '1100':
320
                return None, None
321
            else:
322
                raise BugWatchUpdateError(
323
                    "Mantis APPLICATION ERROR #%s: %s" % (
324
                    app_error_code, app_error_message))
325
326
        bug = {
327
            'id': bug_id,
328
            'status': self._findValueRightOfKey(bug_page, 'Status'),
329
            'resolution': self._findValueRightOfKey(bug_page, 'Resolution')}
330
331
        return int(bug_id), bug
332
333
    def getRemoteBugBatch(self, bug_ids):
334
        """See `ExternalBugTracker`."""
6916.1.4 by Curtis Hovey
Fixed malformed comments found after the end of file loop was fixed in xxxreport.py.
335
        # XXX: Gavin Panella 2007-09-06 bug=137780:
5897.3.1 by Graham Binns
Moved mantis into its own module.
336
        # You may find this zero in "\r\n0" funny. Well I don't. This is
337
        # to work around the fact that Mantis' CSV export doesn't cope
338
        # with the fact that the bug summary can contain embedded "\r\n"
339
        # characters! I don't see a better way to handle this short of
340
        # not using the CSV module and forcing all lines to have the
341
        # same number as fields as the header.
342
        csv_data = self.csv_data.strip().split("\r\n0")
343
344
        if not csv_data:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
345
            raise UnparsableBugData("Empty CSV for %s" % self.baseurl)
5897.3.1 by Graham Binns
Moved mantis into its own module.
346
12505.1.1 by Tim Penhey
Bring back in the mantis fix.
347
        parser = MantisBugBatchParser(csv_data, self._logger)
348
        return parser.getBugs()
5897.3.1 by Graham Binns
Moved mantis into its own module.
349
350
    def _checkForApplicationError(self, page_soup):
351
        """If Mantis does not find the bug it still returns a 200 OK
352
        response, so we need to look into the page to figure it out.
353
354
        If there is no error, None is returned.
355
356
        If there is an error, a 2-tuple of (code, message) is
357
        returned, both unicode strings.
358
        """
359
        app_error = page_soup.find(
360
            text=lambda node: (node.startswith('APPLICATION ERROR ')
361
                               and node.parent['class'] == 'form-title'
362
                               and not isinstance(node, Comment)))
363
        if app_error:
364
            app_error_code = ''.join(c for c in app_error if c.isdigit())
365
            app_error_message = app_error.findNext('p')
366
            if app_error_message is not None:
367
                app_error_message = app_error_message.string
368
            return app_error_code, app_error_message
369
370
        return None
371
372
    def _findValueRightOfKey(self, page_soup, key):
373
        """Scrape a value from a Mantis bug view page where the value
374
        is displayed to the right of the key.
375
376
        The Mantis bug view page uses HTML tables for both layout and
377
        representing tabular data, often within the same table. This
378
        method assumes that the key and value are on the same row,
14645.1.1 by Colin Watson
Fix a slew of typos that have been annoying me.
379
        adjacent to one another, with the key preceding the value:
5897.3.1 by Graham Binns
Moved mantis into its own module.
380
381
        ...
382
        <td>Key</td>
383
        <td>Value</td>
384
        ...
385
386
        This method does not compensate for colspan or rowspan.
387
        """
388
        key_node = page_soup.find(
389
            text=lambda node: (node.strip() == key
390
                               and not isinstance(node, Comment)))
391
        if key_node is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
392
            raise UnparsableBugData("Key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
393
394
        value_cell = key_node.findNext('td')
395
        if value_cell is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
396
            raise UnparsableBugData(
5897.3.1 by Graham Binns
Moved mantis into its own module.
397
                "Value cell for key %r not found." % (key,))
398
399
        value_node = value_cell.string
400
        if value_node is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
401
            raise UnparsableBugData("Value for key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
402
403
        return value_node.strip()
404
405
    def _findValueBelowKey(self, page_soup, key):
406
        """Scrape a value from a Mantis bug view page where the value
407
        is displayed directly below the key.
408
409
        The Mantis bug view page uses HTML tables for both layout and
410
        representing tabular data, often within the same table. This
411
        method assumes that the key and value are within the same
14645.1.1 by Colin Watson
Fix a slew of typos that have been annoying me.
412
        column on adjacent rows, with the key preceding the value:
5897.3.1 by Graham Binns
Moved mantis into its own module.
413
414
        ...
415
        <tr>...<td>Key</td>...</tr>
416
        <tr>...<td>Value</td>...</tr>
417
        ...
418
419
        This method does not compensate for colspan or rowspan.
420
        """
421
        key_node = page_soup.find(
422
            text=lambda node: (node.strip() == key
423
                               and not isinstance(node, Comment)))
424
        if key_node is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
425
            raise UnparsableBugData("Key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
426
427
        key_cell = key_node.parent
428
        if key_cell is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
429
            raise UnparsableBugData("Cell for key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
430
431
        key_row = key_cell.parent
432
        if key_row is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
433
            raise UnparsableBugData("Row for key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
434
435
        try:
436
            key_pos = key_row.findAll('td').index(key_cell)
437
        except ValueError:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
438
            raise UnparsableBugData(
5897.3.1 by Graham Binns
Moved mantis into its own module.
439
                "Key cell in row for key %r not found." % (key,))
440
441
        value_row = key_row.findNextSibling('tr')
442
        if value_row is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
443
            raise UnparsableBugData(
5897.3.1 by Graham Binns
Moved mantis into its own module.
444
                "Value row for key %r not found." % (key,))
445
446
        value_cell = value_row.findAll('td')[key_pos]
447
        if value_cell is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
448
            raise UnparsableBugData(
5897.3.1 by Graham Binns
Moved mantis into its own module.
449
                "Value cell for key %r not found." % (key,))
450
451
        value_node = value_cell.string
452
        if value_node is None:
12221.1.7 by Jeroen Vermeulen
s/Unparseable/Unparsable/g, plus lint.
453
            raise UnparsableBugData("Value for key %r not found." % (key,))
5897.3.1 by Graham Binns
Moved mantis into its own module.
454
455
        return value_node.strip()
456
457
    def getRemoteImportance(self, bug_id):
458
        """See `ExternalBugTracker`.
459
460
        This method is implemented here as a stub to ensure that
461
        existing functionality is preserved. As a result,
462
        UNKNOWN_REMOTE_IMPORTANCE will always be returned.
463
        """
464
        return UNKNOWN_REMOTE_IMPORTANCE
465
466
    def getRemoteStatus(self, bug_id):
467
        if not bug_id.isdigit():
468
            raise InvalidBugId(
469
                "Mantis (%s) bug number not an integer: %s" % (
470
                    self.baseurl, bug_id))
471
472
        try:
473
            bug = self.bugs[int(bug_id)]
474
        except KeyError:
475
            raise BugNotFound(bug_id)
476
477
        # Use a colon and a space to join status and resolution because
478
        # there is a chance that statuses contain spaces, and because
479
        # it makes display of the data nicer.
480
        return "%(status)s: %(resolution)s" % bug
481
482
    def convertRemoteImportance(self, remote_importance):
483
        """See `ExternalBugTracker`.
484
485
        This method is implemented here as a stub to ensure that
486
        existing functionality is preserved. As a result,
487
        BugTaskImportance.UNKNOWN will always be returned.
488
        """
489
        return BugTaskImportance.UNKNOWN
490
6326.8.20 by Gavin Panella
Add titles.
491
    _status_lookup_titles = 'Mantis status', 'Mantis resolution'
6326.8.11 by Gavin Panella
More cleanups.
492
    _status_lookup = (
6326.8.34 by Gavin Panella
Rename Lookup to LookupTree in the externalbugtracker modules.
493
        LookupTree(
6326.8.11 by Gavin Panella
More cleanups.
494
            ('assigned', BugTaskStatus.INPROGRESS),
495
            ('feedback', BugTaskStatus.INCOMPLETE),
496
            ('new', BugTaskStatus.NEW),
497
            ('confirmed', 'ackowledged', BugTaskStatus.CONFIRMED),
6326.8.59 by Gavin Panella
A few small post-review changes.
498
            ('resolved', 'closed',
499
                LookupTree(
6326.8.11 by Gavin Panella
More cleanups.
500
                    ('reopened', BugTaskStatus.NEW),
501
                    ('fixed', 'open', 'no change required',
502
                     BugTaskStatus.FIXRELEASED),
503
                    ('unable to reproduce', 'not fixable', 'suspended',
504
                     'duplicate', BugTaskStatus.INVALID),
505
                    ("won't fix", BugTaskStatus.WONTFIX))),
506
            )
507
        )
508
5897.3.1 by Graham Binns
Moved mantis into its own module.
509
    def convertRemoteStatus(self, status_and_resolution):
6326.8.4 by Gavin Panella
Convert Mantis status conversions.
510
        status, importance = status_and_resolution.split(": ", 1)
511
        try:
6326.8.41 by Gavin Panella
Change __call__ to find in externalbugtracker too.
512
            return self._status_lookup.find(status, importance)
6326.8.4 by Gavin Panella
Convert Mantis status conversions.
513
        except KeyError:
514
            raise UnknownRemoteStatusError(status_and_resolution)