← Back to team overview

gtg team mailing list archive

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

 

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

Requested reviews:
  Gtg developers (gtg)

For more details, see:
https://code.launchpad.net/~qcxhome/gtg/configurable-bugzilla-plugin/+merge/178454
-- 
https://code.launchpad.net/~qcxhome/gtg/configurable-bugzilla-plugin/+merge/178454
Your team Gtg developers is requested to review the proposed merge of lp:~qcxhome/gtg/configurable-bugzilla-plugin into lp:gtg.
=== modified file 'AUTHORS'
--- AUTHORS	2013-06-04 19:29:36 +0000
+++ AUTHORS	2013-08-04 12:44:26 +0000
@@ -117,4 +117,4 @@
 * Tom Kadwill <tomkadwill@xxxxxxxxx>
 * Parin Porecha <parinporecha@xxxxxxxxx>
 * DmDr <cardioexp@xxxxxxxxx>
-* Chenxiong Qi
+* Chenxiong Qi <qcxhome@xxxxxxxxx>

=== modified file 'GTG/plugins/bugzilla/bugzilla.py'
--- GTG/plugins/bugzilla/bugzilla.py	2013-07-02 17:50:18 +0000
+++ GTG/plugins/bugzilla/bugzilla.py	2013-08-04 12:44:26 +0000
@@ -20,9 +20,11 @@
 import xmlrpclib
 from urlparse import urlparse
 
+from dialog import PreferenceDialog
 from services import BugzillaServiceFactory
 from services import BugzillaServiceNotExist
 from notification import send_notification
+from preference import BugzillaPluginPreference
 
 __all__ = ('pluginBugzilla', )
 
@@ -32,9 +34,10 @@
 
 class GetBugInformationTask(threading.Thread):
 
-    def __init__(self, task, **kwargs):
+    def __init__(self, task, preference, **kwargs):
         ''' Initialize task data, where task is the GTG task object. '''
         self.task = task
+        self.preference = preference
         super(GetBugInformationTask, self).__init__(**kwargs)
 
     def parseBugUrl(self, url):
@@ -58,7 +61,8 @@
             return
 
         try:
-            bugzillaService = BugzillaServiceFactory.create(scheme, hostname)
+            bugzillaService = BugzillaServiceFactory.create(
+                scheme, hostname, self.preference['services'])
         except BugzillaServiceNotExist:
             # Stop quietly when bugzilla cannot be found. Currently, I don't
             # assume that user enters a wrong hostname or just an unkown
@@ -87,10 +91,9 @@
             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)
+            tags = self.preference['tags']
+            for tag in tags:
+                gobject.idle_add(self.task.add_tag, '@%s' % tag)
 
 
 class pluginBugzilla:
@@ -106,9 +109,25 @@
         #(anything in a Tree) must be done with gobject.idle_add (invernizzi)
 
         task = self.plugin_api.get_requester().get_task(task_id)
-        bugTask = GetBugInformationTask(task)
+        preference = self.get_preference()
+        bugTask = GetBugInformationTask(task, preference)
         bugTask.setDaemon(True)
         bugTask.start()
 
     def deactivate(self, plugin_api):
         plugin_api.get_ui().disconnect(self.connect_id)
+
+    def is_configurable(self):
+        '''Tell GTG I'm configurable'''
+        return True
+
+    def configure_dialog(self, plugin_manager_dialog):
+        dialog = PreferenceDialog(plugin_manager_dialog)
+        preference = self.get_preference()
+        dialog.run(preference)
+        dialog.destroy()
+
+    def get_preference(self):
+        pref = BugzillaPluginPreference(self.plugin_api)
+        pref.load()
+        return pref

=== added file 'GTG/plugins/bugzilla/dialog.py'
--- GTG/plugins/bugzilla/dialog.py	1970-01-01 00:00:00 +0000
+++ GTG/plugins/bugzilla/dialog.py	2013-08-04 12:44:26 +0000
@@ -0,0 +1,304 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 - Chenxiong Qi <qcxhome@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/>.
+
+import gtk
+
+from GTG import _
+
+__all__ = ('PreferenceDialog', 'ServiceAddDialog', 'ServiceEditDialog')
+
+
+class PreferenceDialog(gtk.Dialog):
+    '''Preference dialog to allow user set their preferences.'''
+
+    title = _('Bugzilla plugin - Preference')
+
+    def __init__(self, parent_dialog):
+        super(self.__class__, self).__init__(
+            self.title,
+            parent_dialog,
+            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+            (gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT,
+             gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
+        self._init_dialog()
+        self._bind_signals()
+
+    def _init_dialog(self):
+        self.set_title(self.title)
+        self.set_size_request(400, 300)
+
+        # Preference body
+        self.ntPreference = gtk.Notebook()
+        self.get_content_area().add(self.ntPreference)
+
+        # Service page
+        self.vbServices = gtk.VBox()
+        self.ntPreference.append_page(self.vbServices,
+                                      tab_label=gtk.Label(_('Service')))
+
+        # Toolbar controlling services
+        self.tlbServiceController = gtk.Toolbar()
+        self.tbnAddService = gtk.ToolButton(gtk.STOCK_NEW)
+        self.tbnEditService = gtk.ToolButton(gtk.STOCK_EDIT)
+        self.tbnDeleteService = gtk.ToolButton(gtk.STOCK_DELETE)
+
+        self.tlbServiceController.insert(self.tbnAddService, 0)
+        self.tlbServiceController.insert(self.tbnEditService, 1)
+        self.tlbServiceController.insert(self.tbnDeleteService, 2)
+        self.vbServices.pack_start(self.tlbServiceController,
+                                   fill=False, expand=False)
+
+        # Services list
+        # First is service name, second is service url, and third one is
+        # enabled.
+        self.services_store = gtk.ListStore(str, str, 'gboolean')
+        self.tvServices = gtk.TreeView(self.services_store)
+
+        # Enabled? column
+        self.service_enabled_renderer = gtk.CellRendererToggle()
+        self.service_enabled_column = gtk.TreeViewColumn(
+            _(''), self.service_enabled_renderer, active=2)
+
+        # Name column
+        self.service_name_renderer = gtk.CellRendererText()
+        self.service_name_column = gtk.TreeViewColumn(
+            _('Name'), self.service_name_renderer, text=0)
+        self.service_name_column.set_resizable(True)
+
+        # Hyperlink column
+        self.service_url_renderer = gtk.CellRendererText()
+        self.service_url_column = gtk.TreeViewColumn(
+            _('URL'), self.service_url_renderer, text=1)
+        self.service_url_column.set_resizable(True)
+
+        self.tvServices.append_column(self.service_enabled_column)
+        self.tvServices.append_column(self.service_name_column)
+        self.tvServices.append_column(self.service_url_column)
+
+        self.scrolled_services_window = gtk.ScrolledWindow()
+        self.scrolled_services_window.set_policy(gtk.POLICY_AUTOMATIC,
+                                                 gtk.POLICY_AUTOMATIC)
+        self.scrolled_services_window.add(self.tvServices)
+        self.vbServices.pack_start(self.scrolled_services_window)
+
+        # Tags page
+        self.vbTags = gtk.VBox()
+        tag_tip = _('Global tags, which will be added to each bug. Tags are '
+                    'separated by a comma, and any white characters '
+                    'surrounding each tag will be removed.')
+        self.lblTagTip = gtk.Label(tag_tip)
+        self.lblTagTip.set_single_line_mode(False)
+        self.lblTagTip.set_line_wrap(True)
+        self.vbTags.pack_start(self.lblTagTip, fill=True, expand=False)
+        self.entTags = gtk.Entry()
+        self.vbTags.pack_start(self.entTags, fill=True, expand=False)
+        self.ntPreference.append_page(self.vbTags,
+                                      tab_label=gtk.Label(_('Tag')))
+
+        self.show_all()
+
+    def _bind_signals(self):
+        self.connect('response', self.response_callback)
+        self.tbnAddService.connect('clicked', self.add_service_callback, None)
+        self.tbnEditService.connect('clicked',
+                                    self.edit_service_callback, None)
+        self.tbnDeleteService.connect('clicked',
+                                      self.delete_service_callback, None)
+
+    def _apply_existing_preference(self):
+        self.entTags.set_text(','.join(self._preference['tags']))
+        services = self._preference['services']
+        for service in services:
+            self.services_store.append((
+                service['name'], service['url'], service['enabled']))
+
+    def run(self, preference):
+        '''Run dialog with existing preference.'''
+        self._preference = preference
+        self._apply_existing_preference()
+        return super(self.__class__, self).run()
+
+    def response_callback(self, dialog, response_id):
+        '''Callback responding response signal to save preference.'''
+        if response_id == gtk.RESPONSE_ACCEPT:
+            self._save()
+
+    def get_service_store(self):
+        return self.services_store
+
+    def get_selected_service(self):
+        selection = self.tvServices.get_selection()
+        return selection.get_selected()
+
+    def add_service_callback(self, widget, data):
+        '''Signal handler for adding new service'''
+        dialog = ServiceAddDialog(self)
+        services_store = self.get_service_store()
+        dialog.run(services_store)
+        dialog.destroy()
+
+    def edit_service_callback(self, widget, data):
+        '''Signal handler for editing a service'''
+        dialog = ServiceEditDialog(self)
+        services_store = self.get_service_store()
+        model, siter = self.get_selected_service()
+        if siter is not None:
+            dialog.run(services_store, siter)
+            dialog.destroy()
+
+    def delete_service_callback(self, widget, data):
+        '''Signal handler for deleting a selected service'''
+        model, siter = self.get_selected_service()
+        if siter is not None:
+            model.remove(siter)
+
+    def _collect_preference(self):
+        '''Collection preference data for preparing to be saved.'''
+        # Tags, ignore empty tag names
+        tags = self.entTags.get_text().split(',')
+        tags = [tag for tag in tags if len(tag) > 0]
+        # Services
+        services = []
+        store = self.get_service_store()
+        siter = store.get_iter_first()
+        while siter is not None:
+            services.append({'name': store.get_value(siter, 0),
+                             'url': store.get_value(siter, 1),
+                             'enabled': store.get_value(siter, 2)})
+            siter = store.iter_next(siter)
+
+        return {'tags': tags, 'services': services}
+
+    def _save(self):
+        '''Save preference'''
+        new_prefs = self._collect_preference()
+        self._preference.update(new_prefs)
+        self._preference.store()
+
+
+class ServiceAddDialog(object):
+    '''Dialog for adding a Bugzilla service.'''
+
+    title = _('Add Service')
+
+    def __init__(self, parent_dialog):
+        self.dialog = gtk.Dialog(
+            self.title,
+            parent_dialog,
+            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+            (gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT,
+             gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
+        self._init_dialog()
+        self._bind_signals()
+
+    def _init_dialog(self):
+        '''Initialize dialog'''
+        self.dialog.set_resizable(False)
+
+        self.label_name = gtk.Label(_('Name'))
+        self.entry_name = gtk.Entry()
+        self.hbox_name = gtk.HBox(spacing=6)
+        self.hbox_name.pack_start(self.label_name, fill=False, expand=False)
+        self.hbox_name.pack_start(self.entry_name)
+
+        self.label_url = gtk.Label(_('URL'))
+        self.entry_url = gtk.Entry()
+        self.hbox_url = gtk.HBox(spacing=6)
+        self.hbox_url.pack_start(self.label_url, fill=False, expand=False)
+        self.hbox_url.pack_start(self.entry_url)
+
+        # Explanation to URL
+        explanation_url = _('URL is a valid URL of this Bugzilla service. '
+                            'For example, https://bugzilla.example.com/, '
+                            'bugzilla.example.com are all acceptable.')
+        self.label_explanation_url = gtk.Label(explanation_url)
+        self.label_explanation_url.set_line_wrap(True)
+        self.label_explanation_url.set_single_line_mode(False)
+        self.hbox_expl_url = gtk.HBox(spacing=6)
+        self.hbox_expl_url.pack_start(gtk.Label(_(' ')), fill=False,
+                                      expand=False)
+        self.hbox_expl_url.pack_start(self.label_explanation_url, fill=False,
+                                      expand=False)
+
+        self.checkbox_enabled = gtk.CheckButton(
+            label=_('Enable this service?'))
+
+        self.vbox_body = gtk.VBox(spacing=6)
+        self.vbox_body.pack_start(self.hbox_name)
+        self.vbox_body.pack_start(self.hbox_url)
+        self.vbox_body.pack_start(self.hbox_expl_url)
+        self.vbox_body.pack_start(self.checkbox_enabled)
+        self.dialog.get_content_area().add(self.vbox_body)
+
+        self.vbox_body.show_all()
+
+    def _bind_signals(self):
+        self.dialog.connect('response', self.response_callback)
+
+    def run(self, services_store):
+        self._services_store = services_store
+        return self.dialog.run()
+
+    def destroy(self):
+        self.dialog.destroy()
+
+    def response_callback(self, dialog, response_id):
+        if response_id == gtk.RESPONSE_ACCEPT:
+            self._save()
+
+    def validate(self):
+        '''Validate service data'''
+
+    def _save(self):
+        '''Save service as a new one by appending to store'''
+        self._services_store.append((
+            self.entry_name.get_text(),
+            self.entry_url.get_text(),
+            self.checkbox_enabled.get_active()))
+
+
+class ServiceEditDialog(ServiceAddDialog):
+    '''Dialog for editing a Bugzilla service.'''
+
+    title = _('Edit Service')
+
+    def _bind_data(self):
+        '''Bind service data to dialog for editing.'''
+        self.entry_name.set_text(
+            self._services_store.get_value(
+                self._service_iter, 0))
+        self.entry_url.set_text(
+            self._services_store.get_value(
+                self._service_iter, 1))
+        self.checkbox_enabled.set_active(
+            self._services_store.get_value(
+                self._service_iter, 2))
+
+    def run(self, services_store, service_iter):
+        '''Run dialog with existing service to edit.'''
+        self._services_store = services_store
+        self._service_iter = service_iter
+        self._bind_data()
+        return self.dialog.run()
+
+    def _save(self):
+        '''Save service data.'''
+        self._services_store.set_value(
+            self._service_iter, 0, self.entry_name.get_text())
+        self._services_store.set_value(
+            self._service_iter, 1, self.entry_url.get_text())
+        self._services_store.set_value(
+            self._service_iter, 2, self.checkbox_enabled.get_active())

=== added file 'GTG/plugins/bugzilla/preference.py'
--- GTG/plugins/bugzilla/preference.py	1970-01-01 00:00:00 +0000
+++ GTG/plugins/bugzilla/preference.py	2013-08-04 12:44:26 +0000
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 - Chenxiong Qi <qcxhome@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/>.
+
+
+class BugzillaPluginPreference(object):
+
+    PLUGIN_NAME = 'bugzilla'
+    DEFAULT_VALUES = {'services': [],
+                      'tags': ['bug']}
+    filename = 'preference'
+
+    def __init__(self, plugin):
+        self.plugin = plugin
+        self._prefs = None
+
+    def load(self):
+        '''Load preference data'''
+        self._prefs = self.plugin.load_configuration_object(
+            self.PLUGIN_NAME, self.filename,
+            default_values=self.DEFAULT_VALUES)
+
+    def store(self):
+        '''Store preference data'''
+        self.plugin.save_configuration_object(self.PLUGIN_NAME,
+                                              self.filename,
+                                              self._prefs)
+
+    def update(self, new_prefs):
+        '''Update preference with new preference values'''
+        self._prefs.update(new_prefs)
+
+    def __getitem__(self, key):
+        return self._prefs[key]
+
+    def __setitem__(self, key, value):
+        self._prefs[key] = value

=== modified file 'GTG/plugins/bugzilla/services.py'
--- GTG/plugins/bugzilla/services.py	2013-06-04 13:54:42 +0000
+++ GTG/plugins/bugzilla/services.py	2013-08-04 12:44:26 +0000
@@ -10,13 +10,24 @@
 
 
 class BugzillaService(object):
-    name = 'Bugzilla Service'
-    enabled = True
-    tag_from = 'component'
+    '''Talking with a real Bugzilla service'''
 
-    def __init__(self, scheme, domain):
+    def __init__(self, scheme, domain, service):
         self.scheme = scheme
         self.domain = domain
+        self.service = service
+
+    @property
+    def name(self):
+        return self.service['name']
+
+    @property
+    def enabled(self):
+        return self.service['enabled']
+
+    @property
+    def url(self):
+        return self.service['url']
 
     def buildXmlRpcServerUrl(self):
         return '%(scheme)s://%(domain)s/xmlrpc.cgi' % {
@@ -32,64 +43,6 @@
         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
@@ -107,11 +60,24 @@
     ''' 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)
+    def create(scheme, domain, services):
+        '''Factory method to create an instance of services.
+
+        scheme: http or https, according to the URL of bug user enters.
+        domain: the domain name of being requested Bugzilla service.
+        services: a list of avialable Bugzilla services information, user
+                  entered in Preference dialog. Each service should provide
+                  two information at least, `enabled` describing whether
+                  service is enabled to get bug information, and url which is
+                  the right URL of specific Bugzilla service hosted by some
+                  project.
+        '''
+        for service in services:
+            # FIXME: due to the URL of a service is not restricted the format,
+            # find method is used here.
+            if service['url'].find(domain) >= 0:
+                if service['enabled']:
+                    return BugzillaService(scheme, domain, service)
+                else:
+                    raise BugzillaServiceDisabled(domain)
+        raise BugzillaServiceNotExist(domain)

=== added file 'GTG/tests/test_plugin_bugzilla.py'
--- GTG/tests/test_plugin_bugzilla.py	1970-01-01 00:00:00 +0000
+++ GTG/tests/test_plugin_bugzilla.py	2013-08-04 12:44:26 +0000
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+
+from GTG.plugins.bugzilla.services import BugzillaServiceFactory
+from GTG.plugins.bugzilla.services import BugzillaServiceDisabled
+from GTG.plugins.bugzilla.services import BugzillaServiceNotExist
+
+
+class BugzillaServiceFactoryTest(unittest.TestCase):
+    '''Test class BugzillaServiceFactory'''
+
+    def setUp(self):
+        self.services = [{'name': 'RedHat Bugzilla',
+                          'url': 'https://bugzilla.redhat.com/',
+                          'enabled': True
+                          },
+                         {'name': 'GNOME Bugzilla',
+                          'url': 'https://bugzilla.gnome.org/',
+                          'enabled': True
+                          },
+                         {'name': 'Freedesktop Bugzilla',
+                          'url': 'https://bugs.freedesktop.org/',
+                          'enabled': False
+                          }]
+
+    def test_create(self):
+        '''A successful operation to get service instance'''
+        scheme = 'https'
+        domain = 'bugzilla.redhat.com'
+        service = BugzillaServiceFactory.create(scheme, domain, self.services)
+        expected_service = self.services[0]
+        self.assertEqual(service.name, expected_service['name'])
+        self.assertEqual(service.url, expected_service['url'])
+        self.assertEqual(service.enabled, expected_service['enabled'])
+
+    def test_service_not_exist(self):
+        scheme = 'https'
+        domain = 'bugzilla.example.com'
+        self.assertRaises(BugzillaServiceNotExist,
+                          BugzillaServiceFactory.create,
+                          scheme, domain, self.services)
+
+    def test_service_disabled(self):
+        scheme = 'https'
+        domain = 'bugs.freedesktop.org'
+        self.assertRaises(BugzillaServiceDisabled,
+                          BugzillaServiceFactory.create,
+                          scheme, domain, self.services)
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)