Subversion Repositories Scribus

Rev

Rev 12495 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

#!/usr/bin/env python
"""This script allows command line bug report submission to a Mantis
bug tracking system (http://www.mantisbt.org/)."""

###
# Copyright (c) 2008, Oleksandr Moskalenko
# All rights reserved.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
###

import re, sys, logging
from mechanize import Browser
from BeautifulSoup import BeautifulSoup

##############################################################################
# USER CONFIGURATION - edit for your site if necessary.                      #
##############################################################################
basetitle = "^Mantis Issue Tracker"
username = None
password = None
baseurl = None
##############################################################################

class Mantisctl:
    """This class handles command line Mantis Bug Tracking System bug report
    submission. a mechanize.Browser() instance is initialized and used to
    control the site navigation and bug report submission. dorun is
    the only public method you really need to run to submit a bug report."""

    def __init__(self):
        self.br = Browser()
        self.br.set_handle_redirect(True)
        self.br.set_handle_referer(True)
        self.br.set_handle_refresh(True, max_time=10.0, honor_time=True)
#        self.br.set_handle_refresh(True, max_time=10.0)
#        self.br.set_handle_refresh(True, honor_time=False)
        (self.opts, self.args) = self.parseopts()

    def _usage(self):
        """Prints out a usage summary and exits."""
        print "\n", 78*"#"
        print """\n
Usage: mantisctl.py [Options], version = mantisctl.py 0.3

  ACTION:
  -T, --test            Test the script
  -L, --list            List the Mantis options
  -r, --report          Report a new bug
  -h, --help            show this help message and exit

  OPTIONS:
  --version             show program's version number and exit
  -n, --noconfirm       Do not ask for confirmation before submitting a report
  -b BASEURL, --baseurl=BASEURL
                        Base URL for a Mantis BTS instance [optional if you set
                        it in the script configuration section]. [default: None]
  -u USERNAME, --username=USERNAME
                        User name [optional if you set it in the script
                        itself]. [default: None]
  -w PASSWORD, --password=PASSWORD
                        User password [optional if you set it in the script
                        itself]. [default: None]
  -a ASSIGNTO, --assignto=ASSIGNTO
                        Assign to [optional]
  -c CATEGORY, --category=CATEGORY
                        Bug category <mandatory>
  -y SEVERITY, --severity=SEVERITY
                        Bug severity <mandatory>
  -s SUMMARY, --summary=SUMMARY
                        Bug summary <mandatory>
  -d DESCRIPTION, --desc=DESCRIPTION
                        Bug description [optional]
  -p PVERSION, --product=PVERSION
                        Product version <mandatory>
  -t TVERSION, --target=TVERSION
                        Target version [optional]
        """

        sys.exit(1)

    def parseopts(self):
        """Parses options and arguments passed to the script
        and returns a tuple of a dictionary (opts) and a list (args)."""

        from optparse import OptionParser
        usage = "%prog [Options], version = %prog 0.3"
        op = OptionParser(usage=usage, version='%prog 0.3')
        op.add_option("-T", "--test", action="store_true", dest="test", default=False, help="Test the script")
        op.add_option("-L", "--list", action="store_true", dest="list", default=False, help="List the Mantis options")
        op.add_option("-r", "--report", action="store_true", dest="report", default=False, help="Report a new bug")
        op.add_option("-n", "--noconfirm", action="store_true", dest="noconfirm", default=False, help="Do not ask for confirmation before submitting a report")
        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].")
        op.add_option("-u", "--username", dest="username", help="User name [optional if you set it in the script itself]. [default: %default]")
        op.add_option("-w", "--password", dest="password", help="User password [optional if you set it in the script itself]. [default: %default]")
        op.add_option("-a", "--assignto", dest="assignto", help="Assign to [optional]")
        op.add_option("-c", "--category", dest="category", help="Bug category <mandatory>")
        op.add_option("-y", "--severity", dest="severity", help="Bug severity <mandatory>")
        op.add_option("-s", "--summary", dest="summary", help="Bug summary <mandatory>")
        op.add_option("-d", "--desc", dest="description", help="Bug description [optional]")
        op.add_option("-p", "--product", dest="pversion", help="Product version <mandatory>")
        op.add_option("-t", "--target", dest="tversion", help="Target version [optional]")
        return op.parse_args()

    def _getbaseurl(self):
        """Returns the Base URL for a Mantis BTS instance from options or
        configuration. Will exit if basurl is not provided either as an option
        or in the configuration section."""

        if not self.opts.baseurl:
            try:
                url = baseurl
            except:
                sys.exit("Base URL is not provided either as an option or in the configuration section.")
        else:
            url = self.opts.baseurl
        return url

    def checkbts(self):
        """Checks if the URL for the Mantis BTS instance front page could be
        opened and if the title of main page corresponds to the configuration
        option 'title'."""

        # Get the base url from options or configuration section
        url = self._getbaseurl()
        try:
            self.br.open(url)
            mainpagetitle = self.br.title()
        except:
            print "Cannot open a Mantis BTS instance main page."
            sys.exit(1)
        # basetitle is defined in the configuration section
        if re.search(basetitle, mainpagetitle):
            return True
        else:
            print "This doesn't seem to be a Mantis BTS instance main page."
            sys.exit(1)

    def askconfirm(self):
        """Asks for a confirmation to submit a bug report. Exits without
        submitting a bug report if 'y' is not provided as an answer."""

        msg = "\nYou are about to submit a bug report with the following data:\n"
        self.printdata(msg)
        yes = ['y', 'Y']
        var = raw_input("Press 'y' to submit the report or any other key to abort and hit ENTER: ")
        if var in yes:
            print "Submitting your bug report..."
        else:
            print "Exiting without submitting the report..."
            sys.exit(0)

    def _printwarning(self, msg, list):
        print "\nYou must specify a %s from the following list: \n" % (msg)
        print list
        sys.exit(1)

    def printdata(self, msg):
        """Prints the bug report data either for verification before and after
        submission."""

        print msg
        print "\tCategory: \t\t%s" % self.category
        print "\tSeverity: \t\t%s" % self.opts.severity
        print "\tAssigned To: \t\t%s" % self.username
        print "\tProduct Version: \t%s" % self.pversion
        print "\tTarget Version: \t%s" % self.tversion
        print "\tSummary: \t\t%s" % self.summary
        print "\tDescription: \t\t%s" % self.description

    def _checkcredentials(self):
        if self.opts.username:
            self.username = self.opts.username
        elif username:
            self.username = username
        else:
            print "\nUsername is not specified either in the script configuration or on the command line."
            self._usage()
        if self.opts.password:
            self.password = self.opts.password
        elif password:
            self.password = password
        else:
            print "\nPassword is not specified either in the script or on the command line."
            self._usage()

    def login(self):
        """Performs logging into a Mantis BTS instance as an authenticated
        reporter."""

        # Check if we have the credentials to login into a Mantis BTS instance.
        self._checkcredentials()
        self.loginlink = self.br.links(text_regex=re.compile("Login")).next()
        self.br.follow_link(self.loginlink)
        self.br.select_form(name="login_form")
        self.br.form["username"] = self.username
        self.br.form["password"] = self.password
        self.br.submit()

    def _gethandlers(self, response):
        """Returns a dictionary of 'Assign To' names and ids directly from the bug report page."""
        response.seek(0)
        handlersoup = BeautifulSoup(response)
        selects = handlersoup.findAll('select', { "name" : "handler_id" })[0].findAll("option")
        handlers = {}
        for select in selects:
            handler_id = select.attrs[0][1].strip().encode('utf8')
            if len (select.contents) > 0:
                handler_login = select.contents[0].strip().encode('utf8')
            else:
                handler_login = ""
            handlers[handler_login] = handler_id
        return handlers

    def _getseverities(self, response):
        """Returns a dictionary of 'Severity' names and ids directly from the bug report page."""
        response.seek(0)
        severitysoup = BeautifulSoup(response)
        selects = severitysoup.findAll('select', { "name" : "severity" })[0].findAll("option")
        severities = {}
        for select in selects:
            severity_id = select.attrs[0][1].strip().encode('utf8')
            if len (select.contents) > 0:
                severity_name = select.contents[0].strip().encode('utf8')
            else:
                severity_name = ""
            severities[severity_name] = severity_id
        return severities

    def _getcategories(self):
        """Returns a list of bug categories directly from the bug reporting page."""
        category_control = self.br.form.find_control("category")
        category_list = []
        for i in category_control.items:
            category_list.append(i.name)
        return category_list

    def _getpversions(self):
        """Returns a list of product versions directly from the bug reporting page."""
        pversion_control = self.br.form.find_control("product_version")
        pversion_list = []
        for i in pversion_control.items:
            pversion_list.append(i.name)
        return pversion_list

    def _gettversions(self):
        """Returns a list of target versions directly from the bug reporting page."""
        tversion_control = self.br.form.find_control("target_version")
        tversion_list = []
        for i in tversion_control.items:
            tversion_list.append(i.name)
        return tversion_list

    def setreportdata(self):
        """Handles filling out the bug report form."""
        reportlink = self.br.links(text_regex=re.compile("Report Issue")).next()
        response = self.br.follow_link(reportlink)
        self.handlers = self._gethandlers(response)
        self.severities = self._getseverities(response)
        self.br.select_form(name="report_bug_form")
        self.categories = self._getcategories()
        self.pversions = self._getpversions()
        self.tversions = self._gettversions()

    def checkreportopts(self):
        """Verifies the validity of options passed to the script."""
        # summary
        if not self.opts.summary:
            sys.exit("You must provide a bug summary!")
        else:
            self.summary = self.opts.summary
        # description
        if self.opts.description and len(self.opts.description.strip()) > 1:
            self.description = self.opts.description
        else:
            self.description = self.opts.summary
        # handler
        if self.opts.assignto and len(self.opts.assignto) > 0:
            if self.opts.assignto not in self.handlers.keys():
                self._printwarning("developer to assign bug to", self.handlers.keys())
            else:
                self.handler = str(self.handlers[self.opts.assignto])
        else:
            self.handler = '0'
        # category
        if not self.opts.category:
            self._printwarning("category", self.categories)
        else:
            if self.opts.category in self.categories:
                self.category = self.opts.category
            else:
                self._printwarning("category", self.categories)
        # severity
        if self.opts.severity and len(self.opts.severity) > 0:
            if self.opts.severity in self.severities.keys():
                self.severity = str(self.severities[self.opts.severity])
            else:
                self._printwarning("severity", self.severities.keys())
        # product version
        if not self.opts.pversion:
            self._printwarning("product version", self.pversions)
        else:
            if self.opts.pversion in self.pversions:
                self.pversion = self.opts.pversion
            else:
                self._printwarning("product version", self.pversions)
        # target version
        if not self.opts.tversion:
            self._printwarning("target version", self.tversions)
        else:
            if self.opts.tversion in self.tversions:
                self.tversion = self.opts.tversion
            else:
                self._printwarning("target version", self.tversions)

    def filloutreportform(self):
        """Fills out required controls on the bug reporting form."""
        self.br.form["category"] = [self.category]
        self.br.form["severity"] = [self.severity]
        self.br.form["handler_id"] = [self.handler]
        self.br.form["product_version"] = [self.pversion]
        if self.tversion:
            self.br.form["target_version"] = [self.tversion]
        self.br.form["summary"] = self.summary
        self.br.form["description"] = self.description

    def comparebugdata(self, soup, data):
        bugs = soup.findAll("table", id="buglist")[0].findAll("tr")[3:-1]
        for bug in bugs:
            bugdata = bug.findAll("td")
#            print bugdata
#            bugno = data[0].contents[0].attrs[2][1].strip().encode('utf-8')
            bugno = bugdata[3].contents[0].contents[0].strip().encode('utf-8')[-4:]
#            print "bug number: %s" % bugno
            try:
                severity = bugdata[8].contents[0].contents[0].strip().encode('utf-8')
            except:
                severity = None
#            print "severity: %s" % severity
            pnumber = bugdata[4].contents[0].strip().encode('utf-8')
#            print "product number: %s" % pnumber
            reporter = bugdata[9].contents[0].strip().encode('utf-8')
#            print "reporter: %s" % reporter
#            print data, [bugno, reporter, severity, pnumber]
            if [reporter, severity, pnumber] == data:
                return bugno
        return None

    def reportbug(self):
        """Handles submission of a bug report."""
        self.setreportdata()
        self.checkreportopts()
        # depends on self.checkreportopts as it sets all self.var(s)
        self.filloutreportform()
        if not self.opts.noconfirm:
            self.askconfirm()
#        try:
#        import sys, logging
#        logger = logging.getLogger("mechanize")
#        logger.addHandler(logging.StreamHandler(sys.stdout))
#        logger.setLevel(logging.DEBUG)
        self.br.set_handle_refresh(True, max_time=2.0, honor_time=True)
        response = self.br.submit()
        response.seek(0)
        reportsoup = BeautifulSoup(response)
#        print reportsoup
#        except:
#            pass
        self.br.close()
#[<td><input type="checkbox" name="bug_arr[]" value="7342" /></td>,
# <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>,
# <td>normal</td>,
# <td><a href="view.php?id=7342">0007342</a></td>,
# <td>1.3.3.12</td>,
# <td></td>,
# <td class="center">&nbsp;</td>,
# <td class="center">Canvas</td>,
# <td class="center"><span class="bold">major</span></td>,
# <td class="center">malex</td>,
# <td class="center"><span class="issue-status" title="open">assigned</span> (malex)</td>,
# <td class="center"><span class="bold">08-08-30</span></td>,
# <td class="left">Summary</td>]
        data = [self.username, self.opts.severity, self.pversion]
        bugno = self.comparebugdata(reportsoup, data)
        if bugno != None:
            msg = "\nBug report number %s submission appears to be successful! The following data have been submitted:\n" % bugno
        else:
            msg = "\nCannot find the bug report. Please check the BTS directly.\n"
        self.printdata(msg)
        sys.exit(0)

    def listopts(self):
        """Lists options passed to the script and those obtained directly from
        a Mantis BTS instance."""

        self.setreportdata()
        self.checkreportopts()
        print "Severities: \n", "\t", self.severities.keys(), "\n"
        print "Categories: \n", "\t", self.categories, "\n"
        print "Developers: \n", "\t", self.handlers.keys(), "\n"
        print "Product Versions: \n", "\t", self.pversions, "\n"

    def runchoice(self):
        """Process a run choice such as list options, test the script, show
        help, or submit a bug report or print usage summary and exit if no
        action has been specified."""

        if self.opts.list:
            print "Listing the Mantis options...\n"
            self.listopts()
        elif self.opts.test:
            print "\nTesting the script...\n",
            self.setreportdata()
            self.checkreportopts()
            print "\nTest run completed without errors.\n"
            sys.exit(0)
        elif self.opts.report:
            print "\nSubmitting a new bug report.\n"
            self.reportbug()
        else:
            print """\nNo action such as -r (submit a bug report), -L (list
            options), or -T (test run) has been specified. Exiting...\n"""

            self._usage()

    def dorun(self):
        """Main controller of the script. It will be called if the script is
        run from a command line or can be run by an external script. No other
        interface is available."""

        # Check if we can access the main page of a Mantis BTS instance.
        self.checkbts()
        # Login into a Mantis BTS instance.
        self.login()
        # Select and run an action.
        self.runchoice()

if __name__=='__main__':
    """Check if the script is run from a command line, initialize the main
    class and either show usage summary or run the main controller method."""

    if len(sys.argv) == 1:
        sys.argv.append("--help")
        browser = Mantisctl()
    else:
        browser = Mantisctl()
        browser.dorun()
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: