Subversion Repositories Scribus

Rev

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">&nbsp;</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: