Rev 12493 | 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" |
30 | #baseurl = None |
||
31 | #username = None |
||
32 | #password = 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) |
||
12494 | malex | 43 | self.br.set_handle_refresh(True, max_time=30.0, honor_time=False) |
12491 | malex | 44 | self.br.set_handle_referer(True) |
12494 | malex | 45 | (self.opts, self.args) = self.parseopts() |
12491 | malex | 46 | |
12494 | malex | 47 | def usage(self): |
48 | """Prints out a usage summary and exits.""" |
||
12493 | malex | 49 | print "\n", 78*"#" |
12491 | malex | 50 | print """\n |
12494 | malex | 51 | Usage: mantisctl.py [Options], version = mantisctl.py 0.3 |
12491 | malex | 52 | |
12494 | malex | 53 | ACTION: |
12491 | malex | 54 | -T, --test Test the script |
55 | -L, --list List the Mantis options |
||
56 | -r, --report Report a new bug |
||
12494 | malex | 57 | -h, --help show this help message and exit |
58 | |||
59 | OPTIONS: |
||
60 | --version show program's version number and exit |
||
12493 | malex | 61 | -n, --noconfirm Do not ask for confirmation before submitting a report |
12494 | malex | 62 | -b BASEURL, --baseurl=BASEURL |
63 | Base URL for a Mantis BTS instance [optional if you set |
||
64 | it in the script configuration section]. [default: None] |
||
12491 | malex | 65 | -u USERNAME, --username=USERNAME |
66 | User name [optional if you set it in the script |
||
67 | itself]. [default: None] |
||
68 | -w PASSWORD, --password=PASSWORD |
||
69 | User password [optional if you set it in the script |
||
70 | itself]. [default: None] |
||
71 | -a ASSIGNTO, --assignto=ASSIGNTO |
||
72 | Assign to [optional] |
||
73 | -c CATEGORY, --category=CATEGORY |
||
74 | Bug category <mandatory> |
||
75 | -y SEVERITY, --severity=SEVERITY |
||
76 | Bug severity <mandatory> |
||
77 | -s SUMMARY, --summary=SUMMARY |
||
78 | Bug summary <mandatory> |
||
79 | -d DESCRIPTION, --desc=DESCRIPTION |
||
80 | Bug description [optional] |
||
81 | -p PVERSION, --product=PVERSION |
||
82 | Product version <mandatory> |
||
83 | -t TVERSION, --target=TVERSION |
||
84 | Target version [optional] |
||
85 | """ |
||
86 | sys.exit(1) |
||
87 | |||
12494 | malex | 88 | def parseopts(self): |
89 | """Parses options and arguments passed to the script |
||
90 | and returns a tuple of a dictionary (opts) and a list (args).""" |
||
12491 | malex | 91 | from optparse import OptionParser |
12494 | malex | 92 | usage = "%prog [Options], version = %prog 0.3" |
93 | op = OptionParser(usage=usage, version='%prog 0.3') |
||
12491 | malex | 94 | op.add_option("-T", "--test", action="store_true", dest="test", default=False, help="Test the script") |
95 | op.add_option("-L", "--list", action="store_true", dest="list", default=False, help="List the Mantis options") |
||
96 | op.add_option("-r", "--report", action="store_true", dest="report", default=False, help="Report a new bug") |
||
12493 | malex | 97 | op.add_option("-n", "--noconfirm", action="store_true", dest="noconfirm", default=False, help="Do not ask for confirmation before submitting a report") |
12494 | malex | 98 | op.add_option("-b", "--baseurl", dest="baseurl", default=baseurl, help="Base URL for a Mantis BTS instance [optional if you set it in the script configuration section]. [default: %default]") |
99 | op.add_option("-u", "--username", dest="username", default=username, help="User name [optional if you set it in the script itself]. [default: %default]") |
||
100 | op.add_option("-w", "--password", dest="password", default=password, help="User password [optional if you set it in the script itself]. [default: %default]") |
||
12491 | malex | 101 | op.add_option("-a", "--assignto", dest="assignto", help="Assign to [optional]") |
102 | op.add_option("-c", "--category", dest="category", help="Bug category <mandatory>") |
||
103 | op.add_option("-y", "--severity", dest="severity", help="Bug severity <mandatory>") |
||
104 | op.add_option("-s", "--summary", dest="summary", help="Bug summary <mandatory>") |
||
105 | op.add_option("-d", "--desc", dest="description", help="Bug description [optional]") |
||
106 | op.add_option("-p", "--product", dest="pversion", help="Product version <mandatory>") |
||
107 | op.add_option("-t", "--target", dest="tversion", help="Target version [optional]") |
||
12493 | malex | 108 | return op.parse_args() |
12491 | malex | 109 | |
12494 | malex | 110 | def _getbaseurl(self): |
111 | """Returns the Base URL for a Mantis BTS instance from options or |
||
112 | configuration. Will exit if basurl is not provided either as an option |
||
113 | or in the confguration section.""" |
||
114 | if not self.opts.baseurl: |
||
115 | try: |
||
116 | url = baseurl |
||
117 | except: |
||
118 | sys.exit("Base URL is not provided either as an option or in the confguration section.") |
||
119 | else: |
||
120 | url = self.opts.baseurl |
||
121 | return baseurl |
||
12491 | malex | 122 | |
12494 | malex | 123 | def checkbts(self): |
124 | """Checks if the URL for the Mantis BTS instance front page could be |
||
125 | opened and if the title of main page corresponds to the configuration |
||
126 | option 'title'.""" |
||
127 | # Get the base url from options or configuration section |
||
128 | url = self._getbaseurl() |
||
129 | try: |
||
130 | self.br.open(url) |
||
131 | mainpagetitle = self.br.title() |
||
132 | except: |
||
133 | print "Cannot open a Mantis BTS instance main page." |
||
134 | sys.exit(1) |
||
135 | # basetitle is defined in the configuration section |
||
136 | if re.search(basetitle, mainpagetitle): |
||
137 | return True |
||
138 | else: |
||
139 | print "This doesn't seem to be a Mantis BTS instance main page." |
||
140 | sys.exit(1) |
||
12491 | malex | 141 | |
12494 | malex | 142 | def askconfirm(self): |
143 | """Asks for a confirmation to submit a bug report. Exits without |
||
144 | submitting a bug report if 'y' is not provided as an answer.""" |
||
145 | msg = "\nYou are about to submit a bug report with the following data:\n" |
||
146 | self.printdata(msg) |
||
147 | yes = ['y', 'Y'] |
||
148 | var = raw_input("Press 'y' to submit the report or any other key to abort and hit ENTER: ") |
||
149 | print var |
||
150 | if var in yes: |
||
151 | print "Submitting your bug report..." |
||
152 | else: |
||
153 | print "Exiting without submitting the report..." |
||
154 | sys.exit(0) |
||
12491 | malex | 155 | |
12494 | malex | 156 | def _printwarning(self, msg, list): |
157 | print "\nYou must specify a %s from the following list: \n" % (msg) |
||
158 | print list |
||
159 | sys.exit(1) |
||
12491 | malex | 160 | |
12494 | malex | 161 | def printdata(self, msg): |
162 | """Prints the bug report data either for verification before and after |
||
163 | submission.""" |
||
164 | print msg |
||
165 | print "\tCategory: \t\t%s" % self.category |
||
166 | print "\tSeverity: \t\t%s" % self.opts.severity |
||
167 | print "\tAssigned To: \t\t%s" % self.username |
||
168 | print "\tProduct Version: \t%s" % self.pversion |
||
169 | print "\tTarget Version: \t%s" % self.tversion |
||
170 | print "\tSummary: \t\t%s" % self.summary |
||
171 | print "\tDescription: \t\t%s" % self.description |
||
172 | |||
173 | def _checkcredentials(self): |
||
174 | if self.opts.username: |
||
175 | self.username = self.opts.username |
||
176 | elif username: |
||
177 | self.username = username |
||
178 | else: |
||
179 | print "\nUsername is not specified either in the script configuration or on the command line." |
||
180 | self._usage() |
||
181 | if self.opts.password: |
||
182 | self.password = self.opts.password |
||
183 | elif password: |
||
184 | self.password = password |
||
185 | else: |
||
186 | print "\nPassword is not specified either in the script or on the command line." |
||
187 | self._usage() |
||
188 | |||
189 | def login(self): |
||
190 | """Performs logging into a Mantis BTS instance as an authenticated |
||
191 | reporter.""" |
||
192 | # Check if we have the credentials to login into a Mantis BTS instance. |
||
193 | self._checkcredentials() |
||
12491 | malex | 194 | self.loginlink = self.br.links(text_regex=re.compile("Login")).next() |
195 | self.br.follow_link(self.loginlink) |
||
196 | self.br.select_form(name="login_form") |
||
12494 | malex | 197 | self.br.form["username"] = self.username |
198 | self.br.form["password"] = self.password |
||
12491 | malex | 199 | self.br.submit() |
200 | |||
12494 | malex | 201 | def _gethandlers(self, response): |
202 | """Returns a dictionary of 'Assign To' names and ids directly from the bug report page.""" |
||
203 | response.seek(0) |
||
204 | handlersoup = BeautifulSoup(response) |
||
205 | selects = handlersoup.findAll('select', { "name" : "handler_id" })[0].findAll("option") |
||
206 | handlers = {} |
||
207 | for select in selects: |
||
208 | handler_id = select.attrs[0][1].strip().encode('utf8') |
||
209 | if len (select.contents) > 0: |
||
210 | handler_login = select.contents[0].strip().encode('utf8') |
||
211 | else: |
||
212 | handler_login = "" |
||
213 | handlers[handler_login] = handler_id |
||
214 | return handlers |
||
12491 | malex | 215 | |
12494 | malex | 216 | def _getseverities(self, response): |
217 | """Returns a dictionary of 'Severity' names and ids directly from the bug report page.""" |
||
218 | response.seek(0) |
||
219 | severitysoup = BeautifulSoup(response) |
||
220 | selects = severitysoup.findAll('select', { "name" : "severity" })[0].findAll("option") |
||
221 | severities = {} |
||
222 | for select in selects: |
||
223 | severity_id = select.attrs[0][1].strip().encode('utf8') |
||
224 | if len (select.contents) > 0: |
||
225 | severity_name = select.contents[0].strip().encode('utf8') |
||
226 | else: |
||
227 | severity_name = "" |
||
228 | severities[severity_name] = severity_id |
||
229 | return severities |
||
12491 | malex | 230 | |
12494 | malex | 231 | def _getcategories(self): |
232 | """Returns a list of bug categories directly from the bug reporting page.""" |
||
233 | category_control = self.br.form.find_control("category") |
||
234 | category_list = [] |
||
235 | for i in category_control.items: |
||
236 | category_list.append(i.name) |
||
237 | return category_list |
||
238 | |||
239 | def _getpversions(self): |
||
240 | """Returns a list of product versions directly from the bug reporting page.""" |
||
241 | pversion_control = self.br.form.find_control("product_version") |
||
242 | pversion_list = [] |
||
243 | for i in pversion_control.items: |
||
244 | pversion_list.append(i.name) |
||
245 | return pversion_list |
||
246 | |||
247 | def _gettversions(self): |
||
248 | """Returns a list of target versions directly from the bug reporting page.""" |
||
249 | tversion_control = self.br.form.find_control("target_version") |
||
250 | tversion_list = [] |
||
251 | for i in tversion_control.items: |
||
252 | tversion_list.append(i.name) |
||
253 | return tversion_list |
||
254 | |||
255 | def setreportdata(self): |
||
256 | """Handles filling out the bug report form.""" |
||
257 | reportlink = self.br.links(text_regex=re.compile("Report Issue")).next() |
||
258 | response = self.br.follow_link(reportlink) |
||
259 | self.handlers = self._gethandlers(response) |
||
260 | self.severities = self._getseverities(response) |
||
261 | self.br.select_form(name="report_bug_form") |
||
262 | self.categories = self._getcategories() |
||
263 | self.pversions = self._getpversions() |
||
264 | self.tversions = self._gettversions() |
||
265 | |||
266 | def checkreportopts(self): |
||
267 | """Verifies the validity of options passed to the script.""" |
||
268 | # summary |
||
12491 | malex | 269 | if not self.opts.summary: |
270 | sys.exit("You must provide a bug summary!") |
||
12494 | malex | 271 | else: |
272 | self.summary = self.opts.summary |
||
273 | # description |
||
274 | if self.opts.description and len(self.opts.description.strip()) > 1: |
||
275 | self.description = self.opts.description |
||
276 | else: |
||
277 | self.description = self.opts.summary |
||
278 | # handler |
||
12491 | malex | 279 | if self.opts.assignto and len(self.opts.assignto) > 0: |
280 | if self.opts.assignto not in self.handlers.keys(): |
||
12494 | malex | 281 | self._printwarning("developer to assign bug to", self.handlers.keys()) |
12491 | malex | 282 | else: |
283 | self.handler = str(self.handlers[self.opts.assignto]) |
||
284 | else: |
||
285 | self.handler = '0' |
||
12494 | malex | 286 | # category |
12491 | malex | 287 | if not self.opts.category: |
12494 | malex | 288 | self._printwarning("category", self.categories) |
12491 | malex | 289 | else: |
12494 | malex | 290 | if self.opts.category in self.categories: |
291 | self.category = self.opts.category |
||
12491 | malex | 292 | else: |
12494 | malex | 293 | self._printwarning("category", self.categories) |
294 | # severity |
||
295 | if self.opts.severity and len(self.opts.severity) > 0: |
||
12491 | malex | 296 | if self.opts.severity in self.severities.keys(): |
12494 | malex | 297 | self.severity = str(self.severities[self.opts.severity]) |
12491 | malex | 298 | else: |
12494 | malex | 299 | self._printwarning("severity", self.severities.keys()) |
300 | # product version |
||
12491 | malex | 301 | if not self.opts.pversion: |
12494 | malex | 302 | self._printwarning("product version", self.pversions) |
12491 | malex | 303 | else: |
12494 | malex | 304 | if self.opts.pversion in self.pversions: |
305 | self.pversion = self.opts.pversion |
||
12491 | malex | 306 | else: |
12494 | malex | 307 | self._printwarning("product version", self.pversions) |
308 | # target version |
||
309 | if not self.opts.tversion: |
||
310 | self._printwarning("target version", self.tversions) |
||
311 | else: |
||
312 | if self.opts.tversion in self.tversions: |
||
313 | self.tversion = self.opts.tversion |
||
12491 | malex | 314 | else: |
12494 | malex | 315 | self._printwarning("target version", self.tversions) |
12491 | malex | 316 | |
12494 | malex | 317 | def filloutreportform(self): |
318 | """Fills out required controls on the bug reporting form.""" |
||
319 | self.br.form["category"] = [self.category] |
||
12491 | malex | 320 | self.br.form["severity"] = [self.severity] |
321 | self.br.form["handler_id"] = [self.handler] |
||
12494 | malex | 322 | self.br.form["product_version"] = [self.pversion] |
323 | if self.tversion: |
||
324 | self.br.form["target_version"] = [self.tversion] |
||
325 | self.br.form["summary"] = self.summary |
||
326 | self.br.form["description"] = self.description |
||
12491 | malex | 327 | |
328 | def reportbug(self): |
||
12494 | malex | 329 | """Handles submission of a bug report.""" |
330 | self.setreportdata() |
||
331 | self.checkreportopts() |
||
332 | # depends on self.checkreportopts as it sets all self.var(s) |
||
333 | self.filloutreportform() |
||
12493 | malex | 334 | if not self.opts.noconfirm: |
12494 | malex | 335 | self.askconfirm() |
12491 | malex | 336 | try: |
337 | self.br.submit() |
||
338 | except: |
||
339 | pass |
||
340 | self.br.close() |
||
12494 | malex | 341 | msg = "\nBug report submission appears to be successful! The following data have been submitted:\n" |
342 | self.printdata(msg) |
||
12491 | malex | 343 | sys.exit(0) |
344 | |||
12494 | malex | 345 | def listopts(self): |
346 | """Lists options passed to the script and those obtained directly from |
||
347 | a Mantis BTS instance.""" |
||
348 | self.setreportdata() |
||
349 | self.checkreportopts() |
||
350 | print "Severities: \n", "\t", self.severities.keys(), "\n" |
||
351 | print "Categories: \n", "\t", self.categories, "\n" |
||
352 | print "Developers: \n", "\t", self.handlers.keys(), "\n" |
||
353 | print "Product Versions: \n", "\t", self.pversions, "\n" |
||
12491 | malex | 354 | |
12494 | malex | 355 | def runchoice(self): |
356 | """Process a run choice such as list options, test the script, show |
||
357 | help, or submit a bug report or print usage summary and exit if no |
||
358 | action has been specified.""" |
||
12491 | malex | 359 | if self.opts.list: |
360 | print "Listing the Mantis options...\n" |
||
12494 | malex | 361 | self.listopts() |
362 | elif self.opts.test: |
||
363 | print "\nTesting the script...\n", |
||
364 | self.setreportdata() |
||
365 | self.checkreportopts() |
||
366 | print "\nTest run completed without errors.\n" |
||
367 | sys.exit(0) |
||
368 | elif self.opts.report: |
||
369 | print "\nSubmitting a new bug report.\n" |
||
12491 | malex | 370 | self.reportbug() |
12494 | malex | 371 | else: |
372 | print """\nNo action such as -r (submit a bug report), -L (list |
||
373 | options), or -T (test run) has been specified. Exiting...\n""" |
||
374 | self._usage() |
||
12491 | malex | 375 | |
12494 | malex | 376 | def dorun(self): |
377 | """Main controller of the script. It will be called if the script is |
||
378 | run from a command line or can be run by an external script. No other |
||
379 | interface is available.""" |
||
380 | # Check if we can access the main page of a Mantis BTS instance. |
||
381 | self.checkbts() |
||
382 | # Login into a Mantis BTS instance. |
||
383 | self.login() |
||
384 | # Select and run an action. |
||
385 | self.runchoice() |
||
386 | |||
12491 | malex | 387 | if __name__=='__main__': |
12494 | malex | 388 | """Check if the script is run from a command line, initialize the main |
389 | class and either show usage summary or run the main controller method.""" |
||
12491 | malex | 390 | if len(sys.argv) == 1: |
391 | sys.argv.append("--help") |
||
12493 | malex | 392 | browser = Mantisctl() |
12491 | malex | 393 | else: |
394 | browser = Mantisctl() |
||
395 | browser.dorun() |
||
396 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: |