gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #03840
[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__)