Rev 12495 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
12491 | malex | 1 | #!/usr/bin/env python |
2 | """This script allows command line bug report submission to a Mantis |
||
3 | bug tracking system (http://www.mantisbt.org/).""" |
||
4 | ### |
||
5 | # Copyright (c) 2008, Oleksandr Moskalenko |
||
6 | # All rights reserved. |
||
7 | # |
||
8 | # This program is free software: you can redistribute it and/or modify |
||
9 | # it under the terms of the GNU General Public License as published by |
||
10 | # the Free Software Foundation, either version 3 of the License, or |
||
11 | # (at your option) any later version. |
||
12 | # |
||
13 | # This program is distributed in the hope that it will be useful, |
||
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
16 | # GNU General Public License for more details. |
||
17 | # |
||
18 | # You should have received a copy of the GNU General Public License |
||
19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
20 | ### |
||
21 | |||
12494 | malex | 22 | import re, sys, logging |
12491 | malex | 23 | from mechanize import Browser |
12494 | malex | 24 | from BeautifulSoup import BeautifulSoup |
12491 | malex | 25 | |
26 | ############################################################################## |
||
12494 | malex | 27 | # USER CONFIGURATION - edit for your site if necessary. # |
12491 | malex | 28 | ############################################################################## |
12494 | malex | 29 | basetitle = "^Mantis Issue Tracker" |
12502 | malex | 30 | username = None |
31 | password = None |
||
32 | baseurl = None |
||
12491 | malex | 33 | ############################################################################## |
34 | |||
35 | class Mantisctl: |
||
12494 | malex | 36 | """This class handles command line Mantis Bug Tracking System bug report |
37 | submission. a mechanize.Browser() instance is initialized and used to |
||
38 | control the site navigation and bug report submission. dorun is |
||
39 | the only public method you really need to run to submit a bug report.""" |
||
12491 | malex | 40 | def __init__(self): |
41 | self.br = Browser() |
||
42 | self.br.set_handle_redirect(True) |
||
43 | self.br.set_handle_referer(True) |
||
12502 | malex | 44 | self.br.set_handle_refresh(True, max_time=10.0, honor_time=True) |
12495 | malex | 45 | # self.br.set_handle_refresh(True, max_time=10.0) |
46 | # self.br.set_handle_refresh(True, honor_time=False) |
||
12494 | malex | 47 | (self.opts, self.args) = self.parseopts() |
12491 | malex | 48 | |
12502 | malex | 49 | def _usage(self): |
12494 | malex | 50 | """Prints out a usage summary and exits.""" |
12493 | malex | 51 | print "\n", 78*"#" |
12491 | malex | 52 | print """\n |
12494 | malex | 53 | Usage: mantisctl.py [Options], version = mantisctl.py 0.3 |
12491 | malex | 54 | |
12494 | malex | 55 | ACTION: |
12491 | malex | 56 | -T, --test Test the script |
57 | -L, --list List the Mantis options |
||
58 | -r, --report Report a new bug |
||
12494 | malex | 59 | -h, --help show this help message and exit |
60 | |||
61 | OPTIONS: |
||
62 | --version show program's version number and exit |
||
12493 | malex | 63 | -n, --noconfirm Do not ask for confirmation before submitting a report |
12494 | malex | 64 | -b BASEURL, --baseurl=BASEURL |
65 | Base URL for a Mantis BTS instance [optional if you set |
||
66 | it in the script configuration section]. [default: None] |
||
12491 | malex | 67 | -u USERNAME, --username=USERNAME |
68 | User name [optional if you set it in the script |
||
69 | itself]. [default: None] |
||
70 | -w PASSWORD, --password=PASSWORD |
||
71 | User password [optional if you set it in the script |
||
72 | itself]. [default: None] |
||
73 | -a ASSIGNTO, --assignto=ASSIGNTO |
||
74 | Assign to [optional] |
||
75 | -c CATEGORY, --category=CATEGORY |
||
76 | Bug category <mandatory> |
||
77 | -y SEVERITY, --severity=SEVERITY |
||
78 | Bug severity <mandatory> |
||
79 | -s SUMMARY, --summary=SUMMARY |
||
80 | Bug summary <mandatory> |
||
81 | -d DESCRIPTION, --desc=DESCRIPTION |
||
82 | Bug description [optional] |
||
83 | -p PVERSION, --product=PVERSION |
||
84 | Product version <mandatory> |
||
85 | -t TVERSION, --target=TVERSION |
||
86 | Target version [optional] |
||
87 | """ |
||
88 | sys.exit(1) |
||
89 | |||
12494 | malex | 90 | def parseopts(self): |
91 | """Parses options and arguments passed to the script |
||
92 | and returns a tuple of a dictionary (opts) and a list (args).""" |
||
12491 | malex | 93 | from optparse import OptionParser |
12494 | malex | 94 | usage = "%prog [Options], version = %prog 0.3" |
95 | op = OptionParser(usage=usage, version='%prog 0.3') |
||
12491 | malex | 96 | op.add_option("-T", "--test", action="store_true", dest="test", default=False, help="Test the script") |
97 | op.add_option("-L", "--list", action="store_true", dest="list", default=False, help="List the Mantis options") |
||
98 | op.add_option("-r", "--report", action="store_true", dest="report", default=False, help="Report a new bug") |
||
12493 | malex | 99 | op.add_option("-n", "--noconfirm", action="store_true", dest="noconfirm", default=False, help="Do not ask for confirmation before submitting a report") |
12495 | malex | 100 | op.add_option("-b", "--baseurl", dest="baseurl", help="Base URL for a Mantis BTS instance [optional if you set it in the script configuration section].") |
101 | op.add_option("-u", "--username", dest="username", help="User name [optional if you set it in the script itself]. [default: %default]") |
||
102 | op.add_option("-w", "--password", dest="password", help="User password [optional if you set it in the script itself]. [default: %default]") |
||
12491 | malex | 103 | op.add_option("-a", "--assignto", dest="assignto", help="Assign to [optional]") |
104 | op.add_option("-c", "--category", dest="category", help="Bug category <mandatory>") |
||
105 | op.add_option("-y", "--severity", dest="severity", help="Bug severity <mandatory>") |
||
106 | op.add_option("-s", "--summary", dest="summary", help="Bug summary <mandatory>") |
||
107 | op.add_option("-d", "--desc", dest="description", help="Bug description [optional]") |
||
108 | op.add_option("-p", "--product", dest="pversion", help="Product version <mandatory>") |
||
109 | op.add_option("-t", "--target", dest="tversion", help="Target version [optional]") |
||
12493 | malex | 110 | return op.parse_args() |
12491 | malex | 111 | |
12494 | malex | 112 | def _getbaseurl(self): |
113 | """Returns the Base URL for a Mantis BTS instance from options or |
||
114 | configuration. Will exit if basurl is not provided either as an option |
||
12495 | malex | 115 | or in the configuration section.""" |
12494 | malex | 116 | if not self.opts.baseurl: |
117 | try: |
||
118 | url = baseurl |
||
119 | except: |
||
12495 | malex | 120 | sys.exit("Base URL is not provided either as an option or in the configuration section.") |
12494 | malex | 121 | else: |
122 | url = self.opts.baseurl |
||
12495 | malex | 123 | return url |
12491 | malex | 124 | |
12494 | malex | 125 | def checkbts(self): |
126 | """Checks if the URL for the Mantis BTS instance front page could be |
||
127 | opened and if the title of main page corresponds to the configuration |
||
128 | option 'title'.""" |
||
129 | # Get the base url from options or configuration section |
||
130 | url = self._getbaseurl() |
||
131 | try: |
||
132 | self.br.open(url) |
||
133 | mainpagetitle = self.br.title() |
||
134 | except: |
||
135 | print "Cannot open a Mantis BTS instance main page." |
||
136 | sys.exit(1) |
||
137 | # basetitle is defined in the configuration section |
||
138 | if re.search(basetitle, mainpagetitle): |
||
139 | return True |
||
140 | else: |
||
141 | print "This doesn't seem to be a Mantis BTS instance main page." |
||
142 | sys.exit(1) |
||
12491 | malex | 143 | |
12494 | malex | 144 | def askconfirm(self): |
145 | """Asks for a confirmation to submit a bug report. Exits without |
||
146 | submitting a bug report if 'y' is not provided as an answer.""" |
||
147 | msg = "\nYou are about to submit a bug report with the following data:\n" |
||
148 | self.printdata(msg) |
||
149 | yes = ['y', 'Y'] |
||
150 | var = raw_input("Press 'y' to submit the report or any other key to abort and hit ENTER: ") |
||
151 | if var in yes: |
||
152 | print "Submitting your bug report..." |
||
153 | else: |
||
154 | print "Exiting without submitting the report..." |
||
155 | sys.exit(0) |
||
12491 | malex | 156 | |
12494 | malex | 157 | def _printwarning(self, msg, list): |
158 | print "\nYou must specify a %s from the following list: \n" % (msg) |
||
159 | print list |
||
160 | sys.exit(1) |
||
12491 | malex | 161 | |
12494 | malex | 162 | def printdata(self, msg): |
163 | """Prints the bug report data either for verification before and after |
||
164 | submission.""" |
||
165 | print msg |
||
166 | print "\tCategory: \t\t%s" % self.category |
||
167 | print "\tSeverity: \t\t%s" % self.opts.severity |
||
168 | print "\tAssigned To: \t\t%s" % self.username |
||
169 | print "\tProduct Version: \t%s" % self.pversion |
||
170 | print "\tTarget Version: \t%s" % self.tversion |
||
171 | print "\tSummary: \t\t%s" % self.summary |
||
172 | print "\tDescription: \t\t%s" % self.description |
||
173 | |||
174 | def _checkcredentials(self): |
||
175 | if self.opts.username: |
||
176 | self.username = self.opts.username |
||
177 | elif username: |
||
178 | self.username = username |
||
179 | else: |
||
180 | print "\nUsername is not specified either in the script configuration or on the command line." |
||
181 | self._usage() |
||
182 | if self.opts.password: |
||
183 | self.password = self.opts.password |
||
184 | elif password: |
||
185 | self.password = password |
||
186 | else: |
||
187 | print "\nPassword is not specified either in the script or on the command line." |
||
188 | self._usage() |
||
189 | |||
190 | def login(self): |
||
191 | """Performs logging into a Mantis BTS instance as an authenticated |
||
192 | reporter.""" |
||
193 | # Check if we have the credentials to login into a Mantis BTS instance. |
||
194 | self._checkcredentials() |
||
12491 | malex | 195 | self.loginlink = self.br.links(text_regex=re.compile("Login")).next() |
196 | self.br.follow_link(self.loginlink) |
||
197 | self.br.select_form(name="login_form") |
||
12494 | malex | 198 | self.br.form["username"] = self.username |
199 | self.br.form["password"] = self.password |
||
12491 | malex | 200 | self.br.submit() |
201 | |||
12494 | malex | 202 | def _gethandlers(self, response): |
203 | """Returns a dictionary of 'Assign To' names and ids directly from the bug report page.""" |
||
204 | response.seek(0) |
||
205 | handlersoup = BeautifulSoup(response) |
||
206 | selects = handlersoup.findAll('select', { "name" : "handler_id" })[0].findAll("option") |
||
207 | handlers = {} |
||
208 | for select in selects: |
||
209 | handler_id = select.attrs[0][1].strip().encode('utf8') |
||
210 | if len (select.contents) > 0: |
||
211 | handler_login = select.contents[0].strip().encode('utf8') |
||
212 | else: |
||
213 | handler_login = "" |
||
214 | handlers[handler_login] = handler_id |
||
215 | return handlers |
||
12491 | malex | 216 | |
12494 | malex | 217 | def _getseverities(self, response): |
218 | """Returns a dictionary of 'Severity' names and ids directly from the bug report page.""" |
||
219 | response.seek(0) |
||
220 | severitysoup = BeautifulSoup(response) |
||
221 | selects = severitysoup.findAll('select', { "name" : "severity" })[0].findAll("option") |
||
222 | severities = {} |
||
223 | for select in selects: |
||
224 | severity_id = select.attrs[0][1].strip().encode('utf8') |
||
225 | if len (select.contents) > 0: |
||
226 | severity_name = select.contents[0].strip().encode('utf8') |
||
227 | else: |
||
228 | severity_name = "" |
||
229 | severities[severity_name] = severity_id |
||
230 | return severities |
||
12491 | malex | 231 | |
12494 | malex | 232 | def _getcategories(self): |
233 | """Returns a list of bug categories directly from the bug reporting page.""" |
||
234 | category_control = self.br.form.find_control("category") |
||
235 | category_list = [] |
||
236 | for i in category_control.items: |
||
237 | category_list.append(i.name) |
||
238 | return category_list |
||
239 | |||
240 | def _getpversions(self): |
||
241 | """Returns a list of product versions directly from the bug reporting page.""" |
||
242 | pversion_control = self.br.form.find_control("product_version") |
||
243 | pversion_list = [] |
||
244 | for i in pversion_control.items: |
||
245 | pversion_list.append(i.name) |
||
246 | return pversion_list |
||
247 | |||
248 | def _gettversions(self): |
||
249 | """Returns a list of target versions directly from the bug reporting page.""" |
||
250 | tversion_control = self.br.form.find_control("target_version") |
||
251 | tversion_list = [] |
||
252 | for i in tversion_control.items: |
||
253 | tversion_list.append(i.name) |
||
254 | return tversion_list |
||
255 | |||
256 | def setreportdata(self): |
||
257 | """Handles filling out the bug report form.""" |
||
258 | reportlink = self.br.links(text_regex=re.compile("Report Issue")).next() |
||
259 | response = self.br.follow_link(reportlink) |
||
260 | self.handlers = self._gethandlers(response) |
||
261 | self.severities = self._getseverities(response) |
||
262 | self.br.select_form(name="report_bug_form") |
||
263 | self.categories = self._getcategories() |
||
264 | self.pversions = self._getpversions() |
||
265 | self.tversions = self._gettversions() |
||
266 | |||
267 | def checkreportopts(self): |
||
268 | """Verifies the validity of options passed to the script.""" |
||
269 | # summary |
||
12491 | malex | 270 | if not self.opts.summary: |
271 | sys.exit("You must provide a bug summary!") |
||
12494 | malex | 272 | else: |
273 | self.summary = self.opts.summary |
||
274 | # description |
||
275 | if self.opts.description and len(self.opts.description.strip()) > 1: |
||
276 | self.description = self.opts.description |
||
277 | else: |
||
278 | self.description = self.opts.summary |
||
279 | # handler |
||
12491 | malex | 280 | if self.opts.assignto and len(self.opts.assignto) > 0: |
281 | if self.opts.assignto not in self.handlers.keys(): |
||
12494 | malex | 282 | self._printwarning("developer to assign bug to", self.handlers.keys()) |
12491 | malex | 283 | else: |
284 | self.handler = str(self.handlers[self.opts.assignto]) |
||
285 | else: |
||
286 | self.handler = '0' |
||
12494 | malex | 287 | # category |
12491 | malex | 288 | if not self.opts.category: |
12494 | malex | 289 | self._printwarning("category", self.categories) |
12491 | malex | 290 | else: |
12494 | malex | 291 | if self.opts.category in self.categories: |
292 | self.category = self.opts.category |
||
12491 | malex | 293 | else: |
12494 | malex | 294 | self._printwarning("category", self.categories) |
295 | # severity |
||
296 | if self.opts.severity and len(self.opts.severity) > 0: |
||
12491 | malex | 297 | if self.opts.severity in self.severities.keys(): |
12494 | malex | 298 | self.severity = str(self.severities[self.opts.severity]) |
12491 | malex | 299 | else: |
12494 | malex | 300 | self._printwarning("severity", self.severities.keys()) |
301 | # product version |
||
12491 | malex | 302 | if not self.opts.pversion: |
12494 | malex | 303 | self._printwarning("product version", self.pversions) |
12491 | malex | 304 | else: |
12494 | malex | 305 | if self.opts.pversion in self.pversions: |
306 | self.pversion = self.opts.pversion |
||
12491 | malex | 307 | else: |
12494 | malex | 308 | self._printwarning("product version", self.pversions) |
309 | # target version |
||
310 | if not self.opts.tversion: |
||
311 | self._printwarning("target version", self.tversions) |
||
312 | else: |
||
313 | if self.opts.tversion in self.tversions: |
||
314 | self.tversion = self.opts.tversion |
||
12491 | malex | 315 | else: |
12494 | malex | 316 | self._printwarning("target version", self.tversions) |
12491 | malex | 317 | |
12494 | malex | 318 | def filloutreportform(self): |
319 | """Fills out required controls on the bug reporting form.""" |
||
320 | self.br.form["category"] = [self.category] |
||
12491 | malex | 321 | self.br.form["severity"] = [self.severity] |
322 | self.br.form["handler_id"] = [self.handler] |
||
12494 | malex | 323 | self.br.form["product_version"] = [self.pversion] |
324 | if self.tversion: |
||
325 | self.br.form["target_version"] = [self.tversion] |
||
326 | self.br.form["summary"] = self.summary |
||
327 | self.br.form["description"] = self.description |
||
12491 | malex | 328 | |
12502 | malex | 329 | def comparebugdata(self, soup, data): |
330 | bugs = soup.findAll("table", id="buglist")[0].findAll("tr")[3:-1] |
||
331 | for bug in bugs: |
||
332 | bugdata = bug.findAll("td") |
||
333 | # print bugdata |
||
334 | # bugno = data[0].contents[0].attrs[2][1].strip().encode('utf-8') |
||
335 | bugno = bugdata[3].contents[0].contents[0].strip().encode('utf-8')[-4:] |
||
336 | # print "bug number: %s" % bugno |
||
337 | try: |
||
338 | severity = bugdata[8].contents[0].contents[0].strip().encode('utf-8') |
||
339 | except: |
||
340 | severity = None |
||
341 | # print "severity: %s" % severity |
||
342 | pnumber = bugdata[4].contents[0].strip().encode('utf-8') |
||
343 | # print "product number: %s" % pnumber |
||
344 | reporter = bugdata[9].contents[0].strip().encode('utf-8') |
||
345 | # print "reporter: %s" % reporter |
||
346 | # print data, [bugno, reporter, severity, pnumber] |
||
347 | if [reporter, severity, pnumber] == data: |
||
348 | return bugno |
||
349 | return None |
||
350 | |||
12491 | malex | 351 | def reportbug(self): |
12494 | malex | 352 | """Handles submission of a bug report.""" |
353 | self.setreportdata() |
||
354 | self.checkreportopts() |
||
355 | # depends on self.checkreportopts as it sets all self.var(s) |
||
356 | self.filloutreportform() |
||
12493 | malex | 357 | if not self.opts.noconfirm: |
12494 | malex | 358 | self.askconfirm() |
12502 | malex | 359 | # try: |
12495 | malex | 360 | # import sys, logging |
361 | # logger = logging.getLogger("mechanize") |
||
362 | # logger.addHandler(logging.StreamHandler(sys.stdout)) |
||
363 | # logger.setLevel(logging.DEBUG) |
||
12502 | malex | 364 | self.br.set_handle_refresh(True, max_time=2.0, honor_time=True) |
365 | response = self.br.submit() |
||
366 | response.seek(0) |
||
367 | reportsoup = BeautifulSoup(response) |
||
368 | # print reportsoup |
||
369 | # except: |
||
370 | # pass |
||
12491 | malex | 371 | self.br.close() |
12502 | malex | 372 | #[<td><input type="checkbox" name="bug_arr[]" value="7342" /></td>, |
373 | # <td><a href="bug_update_page.php?bug_id=7342"><img border="0" width="16" height="16" src="http://bugs.scribus.net/images/update.png" alt="Update Issue" title="Update Issue" /></a></td>, |
||
374 | # <td>normal</td>, |
||
375 | # <td><a href="view.php?id=7342">0007342</a></td>, |
||
376 | # <td>1.3.3.12</td>, |
||
377 | # <td></td>, |
||
378 | # <td class="center"> </td>, |
||
379 | # <td class="center">Canvas</td>, |
||
380 | # <td class="center"><span class="bold">major</span></td>, |
||
381 | # <td class="center">malex</td>, |
||
382 | # <td class="center"><span class="issue-status" title="open">assigned</span> (malex)</td>, |
||
383 | # <td class="center"><span class="bold">08-08-30</span></td>, |
||
384 | # <td class="left">Summary</td>] |
||
385 | data = [self.username, self.opts.severity, self.pversion] |
||
386 | bugno = self.comparebugdata(reportsoup, data) |
||
387 | if bugno != None: |
||
388 | msg = "\nBug report number %s submission appears to be successful! The following data have been submitted:\n" % bugno |
||
389 | else: |
||
390 | msg = "\nCannot find the bug report. Please check the BTS directly.\n" |
||
12494 | malex | 391 | self.printdata(msg) |
12491 | malex | 392 | sys.exit(0) |
393 | |||
12494 | malex | 394 | def listopts(self): |
395 | """Lists options passed to the script and those obtained directly from |
||
396 | a Mantis BTS instance.""" |
||
397 | self.setreportdata() |
||
398 | self.checkreportopts() |
||
399 | print "Severities: \n", "\t", self.severities.keys(), "\n" |
||
400 | print "Categories: \n", "\t", self.categories, "\n" |
||
401 | print "Developers: \n", "\t", self.handlers.keys(), "\n" |
||
402 | print "Product Versions: \n", "\t", self.pversions, "\n" |
||
12491 | malex | 403 | |
12494 | malex | 404 | def runchoice(self): |
405 | """Process a run choice such as list options, test the script, show |
||
406 | help, or submit a bug report or print usage summary and exit if no |
||
407 | action has been specified.""" |
||
12491 | malex | 408 | if self.opts.list: |
409 | print "Listing the Mantis options...\n" |
||
12494 | malex | 410 | self.listopts() |
411 | elif self.opts.test: |
||
412 | print "\nTesting the script...\n", |
||
413 | self.setreportdata() |
||
414 | self.checkreportopts() |
||
415 | print "\nTest run completed without errors.\n" |
||
416 | sys.exit(0) |
||
417 | elif self.opts.report: |
||
418 | print "\nSubmitting a new bug report.\n" |
||
12491 | malex | 419 | self.reportbug() |
12494 | malex | 420 | else: |
421 | print """\nNo action such as -r (submit a bug report), -L (list |
||
422 | options), or -T (test run) has been specified. Exiting...\n""" |
||
423 | self._usage() |
||
12491 | malex | 424 | |
12494 | malex | 425 | def dorun(self): |
426 | """Main controller of the script. It will be called if the script is |
||
427 | run from a command line or can be run by an external script. No other |
||
428 | interface is available.""" |
||
429 | # Check if we can access the main page of a Mantis BTS instance. |
||
430 | self.checkbts() |
||
431 | # Login into a Mantis BTS instance. |
||
432 | self.login() |
||
433 | # Select and run an action. |
||
434 | self.runchoice() |
||
435 | |||
12491 | malex | 436 | if __name__=='__main__': |
12494 | malex | 437 | """Check if the script is run from a command line, initialize the main |
438 | class and either show usage summary or run the main controller method.""" |
||
12491 | malex | 439 | if len(sys.argv) == 1: |
440 | sys.argv.append("--help") |
||
12493 | malex | 441 | browser = Mantisctl() |
12491 | malex | 442 | else: |
443 | browser = Mantisctl() |
||
444 | browser.dorun() |
||
445 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: |