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) |