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 <[email protected]> * Parin Porecha <[email protected]> * DmDr <[email protected]> -* Chenxiong Qi +* Chenxiong Qi <[email protected]> === 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 <[email protected]> +# +# 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 <[email protected]> +# +# 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__)
_______________________________________________ Mailing list: https://launchpad.net/~gtg Post to : [email protected] Unsubscribe : https://launchpad.net/~gtg More help : https://help.launchpad.net/ListHelp

