← Back to team overview

gtg team mailing list archive

[Merge] lp:~qcxhome/gtg/bugzilla-plugin-refactor into lp:gtg

 

Chenxiong Qi has proposed merging lp:~qcxhome/gtg/bugzilla-plugin-refactor into lp:gtg.

Requested reviews:
  Gtg developers (gtg)
Related bugs:
  Bug #1096360 in Getting Things GNOME!: "Refactor Bugzilla plugin to support the differences of each supported Bugzilla service"
  https://bugs.launchpad.net/gtg/+bug/1096360

For more details, see:
https://code.launchpad.net/~qcxhome/gtg/bugzilla-plugin-refactor/+merge/165611
-- 
https://code.launchpad.net/~qcxhome/gtg/bugzilla-plugin-refactor/+merge/165611
Your team Gtg developers is requested to review the proposed merge of lp:~qcxhome/gtg/bugzilla-plugin-refactor into lp:gtg.
=== modified file 'GTG/plugins/bugzilla/bug.py'
--- GTG/plugins/bugzilla/bug.py	2013-02-25 08:12:02 +0000
+++ GTG/plugins/bugzilla/bug.py	2013-05-24 13:15:50 +0000
@@ -14,44 +14,69 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
-# this handles old versions of pybugz as well as new ones
-try:
-    from bugz import bugzilla
-    assert bugzilla
-except:
-    import bugz as bugzilla
-
-# changed the default action to skip auth
-
-
-class Bug:
-
-    def __init__(self, base, nb):
-        # this also handles old versions of pybugz
-        try:
-            bugs = bugzilla.BugzillaProxy(
-                base, skip_auth=True).Bug.get({'ids': [nb, ], })
-        except:
-            bugs = bugzilla.BugzillaProxy(base).Bug.get({'ids': [nb, ], })
-        self.bug = bugs['bugs'][0]
-
-    def get_title(self):
+__all__ = ('BugFactory',)
+
+
+class Bug(object):
+
+    def __init__(self, bug):
+        ''' Initialize Bug object using bug object retrieved via Bugzilla
+            service XMLRPC
+        '''
+        self.bug = bug
+
+    @property
+    def summary(self):
         return self.bug['summary']
 
-    def get_product(self):
+    @property
+    def product(self):
         return self.bug['product']
 
-    def get_component(self):
+    @property
+    def description(self):
+        return self.bug['summary']
+
+    @property
+    def component(self):
         return self.bug['component']
 
-    def get_description(self):
-        return self.bug['summary']
-
-if __name__ == '__main__':
-    for bug in [Bug('https://bugzilla.gnome.org', '598354'),
-                Bug('https://bugs.freedesktop.org', '24120')]:
-        print "title:", bug.get_title()
-        print "product:", bug.get_product()
-        print "component:", bug.get_component()
-        print "description:", bug.get_description()
-        print ""
+
+class GnomeBug(Bug):
+    pass
+
+
+class FreedesktopBug(Bug):
+    pass
+
+
+class GentooBug(Bug):
+    pass
+
+
+class MozillaBug(Bug):
+    pass
+
+
+class SambaBug(Bug):
+    pass
+
+
+class RedHatBug(Bug):
+    pass
+
+
+bugs = {
+    'bugzilla.gnome.org': GnomeBug,
+    'bugs.freedesktop.org': FreedesktopBug,
+    'bugzilla.mozilla.org': MozillaBug,
+    'bugzilla.samba.org': SambaBug,
+    'bugs.gentoo.org': GentooBug,
+    'bugzilla.redhat.com': RedHatBug,
+}
+
+
+class BugFactory(object):
+    @staticmethod
+    def create(serviceDomain, bug):
+        return bugs[serviceDomain](bug)

=== modified file 'GTG/plugins/bugzilla/bugzilla.py'
--- GTG/plugins/bugzilla/bugzilla.py	2013-02-25 07:35:07 +0000
+++ GTG/plugins/bugzilla/bugzilla.py	2013-05-24 13:15:50 +0000
@@ -15,19 +15,71 @@
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import gobject
+import re
 import threading
 import xmlrpclib
 from urlparse import urlparse
 
-from GTG.plugins.bugzilla.server import ServersStore
-from GTG.plugins.bugzilla.bug import Bug
+from services import BugzillaServiceFactory
+from notification import send_notification
+
+__all__ = ('pluginBugzilla', )
+
+bugIdPattern = re.compile('^\d+$')
+
+
+class GetBugInformationTask(threading.Thread):
+
+    def __init__(self, task, **kwargs):
+        ''' Initialize task data, where task is the GTG task object. '''
+        self.task = task
+        super(GetBugInformationTask, self).__init__(**kwargs)
+
+    def parseBugUrl(self, url):
+        r = urlparse(url)
+        queries = dict([item.split('=') for item in r.query.split('&')])
+        return r.scheme, r.hostname, queries
+
+    def run(self):
+        bug_url = self.task.get_title()
+        scheme, hostname, queries = self.parseBugUrl(bug_url)
+
+        bug_id = queries.get('id', None)
+        if bugIdPattern.match(bug_id) is None:
+            # FIXME: make some sensable action instead of returning silently.
+            return
+
+        try:
+            bugzillaService = BugzillaServiceFactory.create(scheme, hostname)
+            bug = bugzillaService.getBug(bug_id)
+        except xmlrpclib.Fault, err:
+            code = err.faultCode
+            if code == 100:  # invalid bug ID
+                title = 'Invalid bug ID #%s' % bug_id
+            elif code == 101:  # bug ID not exist
+                title = 'Bug #%s does not exist.' % bug_id
+            elif code == 102:  # Access denied
+                title = 'Access denied to bug %s' % bug_url
+            else:  # unrecoganized error code currently
+                title = err.faultString
+
+            send_notification(bugzillaService.name, title)
+        except Exception, err:
+            send_notification(bugzillaService.name, err.message)
+        else:
+            title = '#%s: %s' % (bug_id, bug.summary)
+            gobject.idle_add(self.task.set_title, title)
+            text = "%s\n\n%s" % (bug_url, bug.description)
+            gobject.idle_add(self.task.set_text, text)
+
+            tags = bugzillaService.getTags(bug)
+            if tags is not None and tags:
+                for tag in tags:
+                    gobject.idle_add(self.task.add_tag, '@%s' % tag)
 
 
 class pluginBugzilla:
 
-    def __init__(self):
-        self.servers = ServersStore()
-
     def activate(self, plugin_api):
         self.plugin_api = plugin_api
         self.connect_id = plugin_api.get_ui().connect(
@@ -37,62 +89,11 @@
         # this is a gobject callback that will block the Browser.
         # decoupling with a thread. All interaction with task and tags objects
         #(anything in a Tree) must be done with gobject.idle_add (invernizzi)
-        thread = threading.Thread(target=self.__analyze_task,
-                                  args=(task_id, ))
-        thread.setDaemon(True)
-        thread.start()
 
-    def __analyze_task(self, task_id):
         task = self.plugin_api.get_requester().get_task(task_id)
-        url = task.get_title()
-        r = urlparse(url)
-        if r.hostname is None:
-            return
-
-        server = self.servers.get(r.hostname)
-        if server is None:
-            return
-
-        base = '%s://%s/xmlrpc.cgi' % (r.scheme, server.name)
-
-        # get the number of the bug
-        try:
-            nb = r.query.split('id=')[1]
-        except IndexError:
-            return
-
-        try:
-            bug = Bug(base, nb)
-        except xmlrpclib.Fault, err:
-            code = err.faultCode
-            if code == 100:  # invalid bug ID
-                title = 'Invalid bug ID #%s' % nb
-            elif code == 101:  # bug ID not exist
-                title = 'Bug #%s does not exist.' % nb
-            elif code == 102:  # Access denied
-                title = 'Access denied to bug #%s' % nb
-            else:  # unrecoganized error code currently
-                title = err.faultString
-            old_title = task.get_title()
-            gobject.idle_add(task.set_title, title)
-            gobject.idle_add(task.set_text, old_title)
-            return
-        except:
-            return
-
-        title = bug.get_title()
-        if title is None:
-            # can't find the title of the bug
-            return
-
-        gobject.idle_add(task.set_title, '#%s: %s' % (nb, title))
-
-        text = "%s\n\n%s" % (url, bug.get_description())
-        gobject.idle_add(task.set_text, text)
-
-        tag = server.get_tag(bug)
-        if tag is not None:
-            gobject.idle_add(task.add_tag, '@%s' % tag)
+        bugTask = GetBugInformationTask(task)
+        bugTask.setDaemon(True)
+        bugTask.start()
 
     def deactivate(self, plugin_api):
         plugin_api.get_ui().disconnect(self.connect_id)

=== added file 'GTG/plugins/bugzilla/notification.py'
--- GTG/plugins/bugzilla/notification.py	1970-01-01 00:00:00 +0000
+++ GTG/plugins/bugzilla/notification.py	2013-05-24 13:15:50 +0000
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+'''
+Notification is used to show messages to GTG users.
+'''
+
+import atexit
+import subprocess
+
+__all__ = ("send_notification", )
+
+APP_NAME = "GTG"
+# How many millisecond the notification area lasts
+TIMEOUT = 3000
+
+
+def _notify_via_pynotify(title, message):
+    pynotify.init(APP_NAME)
+    nt = pynotify.Notification(title, message)
+    nt.set_timeout(TIMEOUT)
+    nt.show()
+
+
+def _notify_via_notify_send(title, message):
+    cmd = "notify-send --app-name=%s --expire-time=%d \"%s\" \"%s\"" % (
+        APP_NAME, TIMEOUT, title, message)
+    proc = subprocess.Popen(cmd, shell=True)
+
+
+# A reference to the concrete handler that sends notification.
+# By default, this reference is set to None in case all candidates are not
+# available to keep silient when unexpected things happen.
+_notify_handler = None
+try:
+    # Primarily, pynotify is used to send notification. However, it might not
+    # appear in user's machine. So, we'll try another alternative.
+    import pynotify
+    _notify_handler = _notify_via_pynotify
+except ImportError:
+    # The alternative is notify-send, which is a command line utility provided
+    # by libnotify package.
+    proc = subprocess.Popen("which notify-send", shell=True)
+    if proc.wait() == 0:
+        _notify_handler = _notify_via_notify_send
+
+def send_notification(title, message):
+    ''' A proxy to send notification
+
+    When no notification utility is available, just keep silent.
+    '''
+
+    if _notify_handler is not None:
+        _notify_handler(title, message)
+
+
+@atexit.register
+def uinit_pynotify():
+    pynotify.uninit()

=== removed file 'GTG/plugins/bugzilla/server.py'
--- GTG/plugins/bugzilla/server.py	2012-07-13 17:24:28 +0000
+++ GTG/plugins/bugzilla/server.py	1970-01-01 00:00:00 +0000
@@ -1,70 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Guillaume Desmottes <gdesmott@xxxxxxxxx>
-#
-# 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/>.
-
-SERVER_TAG_PRODUCT = 1
-SERVER_TAG_COMPONENT = 2
-
-
-class ServersStore:
-
-    def __init__(self):
-        self.servers = {}
-
-        # GNOME
-        server = Server('bugzilla.gnome.org')
-        server.tag = SERVER_TAG_PRODUCT
-        self.add(server)
-
-        # freedesktop.org
-        server = Server('bugs.freedesktop.org')
-        server.tag = SERVER_TAG_COMPONENT
-        self.add(server)
-
-        # Mozilla
-        server = Server('bugzilla.mozilla.org')
-        server.tag = SERVER_TAG_COMPONENT
-        self.add(server)
-
-        # Samba
-        server = Server('bugzilla.samba.org')
-        server.tag = SERVER_TAG_COMPONENT
-        self.add(server)
-
-        # GENTOO
-        server = Server('bugs.gentoo.org')
-        server.tag = SERVER_TAG_COMPONENT
-        self.add(server)
-
-    def add(self, server):
-        self.servers[server.name] = server
-
-    def get(self, name):
-        return self.servers.get(name)
-
-
-class Server:
-
-    def __init__(self, name):
-        self.name = name
-        self.tag = None
-
-    def get_tag(self, bug):
-        if self.tag is None:
-            return None
-        elif self.tag == SERVER_TAG_PRODUCT:
-            return bug.get_product()
-        elif self.tag == SERVER_TAG_COMPONENT:
-            return bug.get_component()

=== added file 'GTG/plugins/bugzilla/services.py'
--- GTG/plugins/bugzilla/services.py	1970-01-01 00:00:00 +0000
+++ GTG/plugins/bugzilla/services.py	2013-05-24 13:15:50 +0000
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+# Remove dependence of bugz due to that plugin just needs get action and
+# it is done by Python xmlrpclib simply enough.
+from xmlrpclib import ServerProxy
+
+from bug import BugFactory
+
+__all__ = ('BugzillaServiceFactory',)
+
+
+class BugzillaService(object):
+    name = 'Bugzilla Service'
+    enabled = True
+    tag_from = 'component'
+
+    def __init__(self, scheme, domain):
+        self.scheme = scheme
+        self.domain = domain
+
+    def buildXmlRpcServerUrl(self):
+        return '%(scheme)s://%(domain)s/xmlrpc.cgi' % {
+            'scheme': self.scheme, 'domain': self.domain,
+        }
+
+    def getProxy(self, server_url):
+        return ServerProxy(server_url)
+
+    def getBug(self, bug_id):
+        server_url = self.buildXmlRpcServerUrl()
+        proxy = self.getProxy(server_url)
+        bugs = proxy.Bug.get({'ids': [bug_id, ]})
+        return BugFactory.create(self.domain, bugs['bugs'][0])
+
+    def getTags(self, bug):
+        ''' Get a list of tags due to some bug attribute contains list rather
+            than a string in some bugzilla service.
+        '''
+        tag_names = getattr(bug, self.tag_from, None)
+        if tag_names is None:
+            return []
+        if not isinstance(tag_names, list):
+            return [tag_names]
+        return tag_names
+
+
+class GnomeBugzilla(BugzillaService):
+    name = 'GNOME Bugzilla Service'
+    tag_from = 'product'
+
+
+class FreedesktopBugzilla(BugzillaService):
+    ''' Bugzilla service of Freedesktop projects '''
+
+    name = 'Freedesktop Bugzilla Service'
+
+class GentooBugzilla(BugzillaService):
+    ''' Bugzilla service of Gentoo project '''
+
+    name = 'Gentoo Bugzilla Service'
+
+class MozillaBugzilla(BugzillaService):
+    ''' Bugzilla service of Mozilla products '''
+
+    name = 'Mozilla Bugzilla Service'
+
+class SambaBugzilla(BugzillaService):
+    ''' Bugzilla service of Samba project '''
+
+    enabled = False
+    name = 'Samba Bugzilla Service'
+
+
+class RedHatBugzilla(BugzillaService):
+    ''' Bugzilla service provided by Red Hat '''
+
+    name = 'Red Hat Bugzilla Service'
+
+# Register bugzilla services manually, however store them in someplace and load
+# them at once is better.
+services = {
+    'bugzilla.gnome.org': GnomeBugzilla,
+    'bugs.freedesktop.org': FreedesktopBugzilla,
+    'bugzilla.mozilla.org': MozillaBugzilla,
+    'bugzilla.samba.org': SambaBugzilla,
+    'bugs.gentoo.org': GentooBugzilla,
+    'bugzilla.redhat.com': RedHatBugzilla,
+}
+
+
+class BugzillaServiceNotExist(Exception):
+    pass
+
+
+class BugzillaServiceDisabled(Exception):
+    ''' Bugzilla service is disabled by user. '''
+
+    def __init__(self, domain, *args, **kwargs):
+        self.message = '%s is disabled.' % domain
+        super(BugzillaServiceDisabled, self).__init__(*args, **kwargs)
+
+
+class BugzillaServiceFactory(object):
+    ''' Create a Bugzilla service using scheme and domain '''
+
+    @staticmethod
+    def create(scheme, domain):
+        if domain in services:
+            service = services[domain]
+            if not service.enabled:
+                raise BugzillaServiceDisabled(domain)
+            return services[domain](scheme, domain)
+        else:
+            raise BugzillaServiceNotExist(domain)