← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/remote-bugtracker-components-ui-0 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/remote-bugtracker-components-ui-0 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/remote-bugtracker-components-ui-0/+merge/63392

Implements UI for displaying components registered at a remote bug tracker

    Launchpad bug: https://bugs.launchpad.net/bugs/617695
    Pre-implementation: Bryce

This branch cleans up Bryce's branch so that it can be landed.

--------------------------------------------------------------------

RULES

    * Resolve conflicts with devel.
    * Moved code out the othe deprecated canonical.launchpad path.
    * Fix failing tests.
    * Refactor the implementation parts called out in the review that
      can be implemented in a simpler way.
    * ADDENDUM:
      * Register feture flag bugs.bugtracker_components.enabled
      * Fixed invalid storm find() clauses
      * Allow users to unset the a component.


QA

    * After cronscripts/update-bugzilla-remote-components.py has run
      and the feature is enabled:
      bugs.bugtracker_components.enabled  team:launchpad-beta-testers  1  on)
      http://people.canonical.com/~curtis/components.png


LINT

    cronscripts/update-bugzilla-remote-components.py
    lib/canonical/launchpad/interfaces/_schema_circular_imports.py
    lib/lp/bugs/configure.zcml
    lib/lp/bugs/browser/bugtracker.py
    lib/lp/bugs/browser/configure.zcml
    lib/lp/bugs/browser/tests/test_bugtracker_component.py
    lib/lp/bugs/browser/widgets/bugtask.py
    lib/lp/bugs/interfaces/bugtracker.py
    lib/lp/bugs/model/bugtracker.py
    lib/lp/bugs/scripts/bzremotecomponentfinder.py
    lib/lp/bugs/templates/bugtracker-index.pt
    lib/lp/bugs/templates/bugtracker-portlet-components.pt
    lib/lp/bugs/tests/test_bugtracker_components.py
    lib/lp/bugs/tests/test_bzremotecomponentfinder.py
    lib/lp/services/features/flags.py


TEST

    ./bin/test -vv -t test_bugtracker_component


IMPLEMENTATION

I registered a feature flag and fixed the layout in the bug tracker template.
    lib/lp/services/features/flags.py
    lib/lp/bugs/templates/bugtracker-index.pt

Revised the layout of the components. This feature is born in a pathological
state; there are too many items listed for a human to read. I made the layout
use the full horizontal space, but I would not call this useable yet.
    lib/lp/bugs/templates/bugtracker-portlet-components.pt

I discovered that the storm find clauses in this module were invalid. they
where assignment (=) instead or equality (==). I fixed these.
    lib/lp/bugs/model/bugtracker.py

The edit form was missing a cancel link and setting the components dsp to
None caused an oops.
    lib/lp/bugs/browser/bugtracker.py

Bryces's work with my fixes from the review.
    cronscripts/update-bugzilla-remote-components.py
    lib/canonical/launchpad/interfaces/_schema_circular_imports.py
    lib/lp/bugs/configure.zcml
    lib/lp/bugs/browser/configure.zcml
    lib/lp/bugs/browser/tests/test_bugtracker_component.py
    lib/lp/bugs/browser/widgets/bugtask.py
    lib/lp/bugs/interfaces/bugtracker.py
    lib/lp/bugs/scripts/bzremotecomponentfinder.py
    lib/lp/bugs/tests/test_bugtracker_components.py
    lib/lp/bugs/tests/test_bzremotecomponentfinder.py
-- 
https://code.launchpad.net/~sinzui/launchpad/remote-bugtracker-components-ui-0/+merge/63392
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/remote-bugtracker-components-ui-0 into lp:launchpad.
=== modified file 'cronscripts/update-bugzilla-remote-components.py'
--- cronscripts/update-bugzilla-remote-components.py	2010-10-19 23:55:47 +0000
+++ cronscripts/update-bugzilla-remote-components.py	2011-06-03 15:12:07 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/python -S
 #
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=W0403
@@ -10,7 +10,7 @@
 
 from canonical.config import config
 from lp.services.scripts.base import LaunchpadCronScript
-from canonical.launchpad.scripts.bzremotecomponentfinder import (
+from lp.bugs.scripts.bzremotecomponentfinder import (
     BugzillaRemoteComponentFinder,
     )
 

=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-06-01 20:12:45 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-06-03 15:12:07 +0000
@@ -618,6 +618,9 @@
     IBugTracker, 'addRemoteComponentGroup', IBugTrackerComponentGroup)
 patch_collection_return_type(
     IBugTracker, 'getAllRemoteComponentGroups', IBugTrackerComponentGroup)
+patch_entry_return_type(
+    IBugTracker, 'getRemoteComponentForDistroSourcePackageName',
+    IBugTrackerComponent)
 
 ## IBugTrackerComponent
 patch_reference_property(

=== modified file 'lib/lp/bugs/browser/bugtracker.py'
--- lib/lp/bugs/browser/bugtracker.py	2011-05-27 21:12:25 +0000
+++ lib/lp/bugs/browser/bugtracker.py	2011-06-03 15:12:07 +0000
@@ -10,6 +10,7 @@
     'BugTrackerBreadcrumb',
     'BugTrackerComponentGroupNavigation',
     'BugTrackerEditView',
+    'BugTrackerEditComponentView',
     'BugTrackerNavigation',
     'BugTrackerNavigationMenu',
     'BugTrackerSetBreadcrumb',
@@ -65,9 +66,13 @@
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
 from lp.app.widgets.textwidgets import DelimitedListWidget
+from lp.bugs.browser.widgets.bugtask import (
+    UbuntuSourcePackageNameWidget,
+    )
 from lp.bugs.interfaces.bugtracker import (
     BugTrackerType,
     IBugTracker,
+    IBugTrackerComponent,
     IBugTrackerComponentGroup,
     IBugTrackerSet,
     IRemoteBug,
@@ -227,6 +232,11 @@
         return shortlist(chain(self.context.projects,
                                self.context.products), 100)
 
+    @property
+    def related_component_groups(self):
+        """All component groups and components."""
+        return self.context.getAllRemoteComponentGroups()
+
 
 BUG_TRACKER_ACTIVE_VOCABULARY = SimpleVocabulary.fromItems(
     [('On', True), ('Off', False)])
@@ -445,16 +455,89 @@
             return RemoteBug(self.context, remotebug, bugs)
 
     @stepthrough("+components")
-    def component_groups(self, name):
-        return self.context.getRemoteComponentGroup(name)
+    def component_groups(self, id):
+        # Navigate by id (component group name should work too)
+        return self.context.getRemoteComponentGroup(id)
+
+
+class BugTrackerEditComponentView(LaunchpadEditFormView):
+    """Provides editing form for setting source packages for components.
+
+    In this class we assume that bug tracker components are always
+    linked to source packages in the Ubuntu distribution.
+    """
+    schema = IBugTrackerComponent
+    custom_widget('sourcepackagename', UbuntuSourcePackageNameWidget)
+    field_names = ['sourcepackagename']
+    page_title = 'Link component'
+
+    @property
+    def label(self):
+        return (
+            'Link a distribution source package to %s component' %
+            self.context.name)
+
+    @property
+    def initial_values(self):
+        """See `LaunchpadFormView.`"""
+        field_values = dict(sourcepackagename='')
+        dsp = self.context.distro_source_package
+        if dsp is not None:
+            field_values['sourcepackagename'] = dsp.name
+        return field_values
+
+    @property
+    def next_url(self):
+        return canonical_url(self.context.component_group.bug_tracker)
+
+    cancel_url = next_url
+
+    def updateContextFromData(self, data, context=None):
+        """Link component to specified distro source package.
+
+        Get the user-provided source package name from the form widget,
+        look it up in Ubuntu to retrieve the distro_source_package
+        object, and link it to this component.
+        """
+        sourcepackagename = data['sourcepackagename']
+        distribution = self.widgets['sourcepackagename'].getDistribution()
+        dsp = distribution.getSourcePackage(sourcepackagename)
+        bug_tracker = self.context.component_group.bug_tracker
+        # Has this source package already been assigned to a component?
+        component = bug_tracker.getRemoteComponentForDistroSourcePackageName(
+            distribution, sourcepackagename)
+        if component is not None:
+            self.request.response.addNotification(
+                "The %s source package is already linked to %s:%s in %s." % (
+                    sourcepackagename.name,
+                    component.component_group.name,
+                    component.name, distribution.name))
+            return
+        # The submitted component can be linked to the distro source package.
+        component = context or self.context
+        component.distro_source_package = dsp
+        if sourcepackagename is None:
+            self.request.response.addNotification(
+                "%s:%s is now unlinked." % (
+                    component.component_group.name, component.name))
+        else:
+            self.request.response.addNotification(
+                "%s:%s is now linked to the %s source package in %s." % (
+                    component.component_group.name, component.name,
+                    sourcepackagename.name, distribution.name))
+
+    @action('Save Changes', name='save')
+    def save_action(self, action, data):
+        """Update the component with the form data."""
+        self.updateContextFromData(data)
 
 
 class BugTrackerComponentGroupNavigation(Navigation):
 
     usedfor = IBugTrackerComponentGroup
 
-    def traverse(self, name):
-        return self.context.getComponent(name)
+    def traverse(self, id):
+        return self.context.getComponent(id)
 
 
 class BugTrackerSetBreadcrumb(Breadcrumb):

=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml	2011-05-27 18:10:50 +0000
+++ lib/lp/bugs/browser/configure.zcml	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2010 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -813,6 +813,12 @@
             path_expression="name"
             attribute_to_parent="component_group"
             rootsite="bugs"/>
+        <browser:page
+            name="+edit"
+            for="lp.bugs.interfaces.bugtracker.IBugTrackerComponent"
+            class="lp.bugs.browser.bugtracker.BugTrackerEditComponentView"
+            permission="launchpad.AnyPerson"
+            template="../../app/templates/generic-edit.pt"/>
         <browser:pages
             for="lp.bugs.interfaces.bugtracker.IBugTracker"
             class="lp.bugs.browser.bugtracker.BugTrackerView"
@@ -824,6 +830,9 @@
                 name="+portlet-details"
                 template="../templates/bugtracker-portlet-details.pt"/>
             <browser:page
+                name="+portlet-components"
+                template="../templates/bugtracker-portlet-components.pt"/>
+            <browser:page
                 name="+portlet-projects"
                 template="../templates/bugtracker-portlet-projects.pt"/>
             <browser:page

=== added file 'lib/lp/bugs/browser/tests/test_bugtracker_component.py'
--- lib/lp/bugs/browser/tests/test_bugtracker_component.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/tests/test_bugtracker_component.py	2011-06-03 15:12:07 +0000
@@ -0,0 +1,124 @@
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version (see the file LICENSE).
+
+"""Unit tests for linking bug tracker components to source packages."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+from canonical.launchpad.webapp.publisher import canonical_url
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.testing import (
+    login_person,
+    TestCaseWithFactory,
+    )
+from lp.testing.views import create_initialized_view
+
+
+class BugTrackerEditComponentViewTextCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(BugTrackerEditComponentViewTextCase, self).setUp()
+        regular_user = self.factory.makePerson()
+        login_person(regular_user)
+
+        self.bug_tracker = self.factory.makeBugTracker()
+        self.comp_group = self.factory.makeBugTrackerComponentGroup(
+            u'alpha', self.bug_tracker)
+
+    def _makeForm(self, sourcepackage):
+        if sourcepackage is None:
+            name = ''
+        else:
+            name = sourcepackage.name
+        return {
+            'field.sourcepackagename': name,
+            'field.actions.save': 'Save',
+            }
+
+    def test_view_attributes(self):
+        component = self.factory.makeBugTrackerComponent(
+            u'Example', self.comp_group)
+        distro = getUtility(IDistributionSet).getByName('ubuntu')
+        package = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='example', distribution=distro)
+        form = self._makeForm(package)
+        view = create_initialized_view(
+            component, name='+edit', form=form)
+        label = 'Link a distribution source package to Example component'
+        self.assertEqual(label, view.label)
+        self.assertEqual('Link component', view.page_title)
+        self.assertEqual(['sourcepackagename'], view.field_names)
+        url = canonical_url(component.component_group.bug_tracker)
+        self.assertEqual(url, view.next_url)
+        self.assertEqual(url, view.cancel_url)
+
+    def test_linking(self):
+        component = self.factory.makeBugTrackerComponent(
+            u'Example', self.comp_group)
+        distro = getUtility(IDistributionSet).getByName('ubuntu')
+        package = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='example', distribution=distro)
+
+        self.assertIs(None, component.distro_source_package)
+        form = self._makeForm(package)
+        view = create_initialized_view(
+            component, name='+edit', form=form)
+        self.assertEqual([], view.errors)
+
+        notifications = view.request.response.notifications
+        self.assertEqual(component.distro_source_package, package)
+        expected = (
+            u"alpha:Example is now linked to the example "
+            "source package in ubuntu.")
+        self.assertEqual(expected, notifications.pop().message)
+
+    def test_unlinking(self):
+        component = self.factory.makeBugTrackerComponent(
+            u'Example', self.comp_group)
+        distro = getUtility(IDistributionSet).getByName('ubuntu')
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='example', distribution=distro)
+        component.distro_source_package = dsp
+        form = self._makeForm(None)
+        view = create_initialized_view(
+            component, name='+edit', form=form)
+        self.assertEqual([], view.errors)
+        notifications = view.request.response.notifications
+        self.assertEqual(None, component.distro_source_package)
+        expected = "alpha:Example is now unlinked."
+        self.assertEqual(expected, notifications.pop().message)
+
+    def test_cannot_doublelink_sourcepackages(self):
+        # Two components try linking to same package
+        component_a = self.factory.makeBugTrackerComponent(
+            u'a', self.comp_group)
+        component_b = self.factory.makeBugTrackerComponent(
+            u'b', self.comp_group)
+        distro = getUtility(IDistributionSet).getByName('ubuntu')
+        package = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='example', distribution=distro)
+
+        form = self._makeForm(package)
+        view = create_initialized_view(
+            component_a, name='+edit', form=form)
+        notifications = view.request.response.notifications
+        self.assertEqual([], view.errors)
+        self.assertEqual(1, len(notifications))
+        self.assertEqual(package, component_a.distro_source_package)
+
+        form = self._makeForm(package)
+        view = create_initialized_view(
+            component_b, name='+edit', form=form)
+        self.assertIs(None, component_b.distro_source_package)
+        self.assertEqual([], view.errors)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected = (
+            "The example source package is already linked to "
+            "alpha:a in ubuntu.")
+        self.assertEqual(expected, notifications.pop().message)

=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
--- lib/lp/bugs/browser/widgets/bugtask.py	2011-02-02 15:43:31 +0000
+++ lib/lp/bugs/browser/widgets/bugtask.py	2011-06-03 15:12:07 +0000
@@ -14,6 +14,7 @@
     "DBItemDisplayWidget",
     "NewLineToSpacesWidget",
     "NominationReviewActionWidget",
+    "UbuntuSourcePackageNameWidget",
     ]
 
 from xml.sax.saxutils import escape
@@ -54,6 +55,7 @@
     NotFoundError,
     UnexpectedFormData,
     )
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.widgets.helpers import get_widget_template
 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
 from lp.app.widgets.popup import VocabularyPickerWidget
@@ -527,6 +529,14 @@
         return distribution
 
 
+class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
+    """A widget to select Ubuntu packages."""
+
+    def getDistribution(self):
+        """See `BugTaskSourcePackageNameWidget`"""
+        return getUtility(ILaunchpadCelebrities).ubuntu
+
+
 class AssigneeDisplayWidget(BrowserWidget):
     """A widget for displaying an assignee."""
 

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2011-04-13 18:48:42 +0000
+++ lib/lp/bugs/configure.zcml	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -394,6 +394,7 @@
                     getBugFilingAndSearchLinks
                     getBugsWatching
                     getLinkedPersonByName
+                    getRemoteComponentForDistroSourcePackageName
                     getRemoteComponentGroup
                     has_lp_plugin
                     id

=== modified file 'lib/lp/bugs/interfaces/bugtracker.py'
--- lib/lp/bugs/interfaces/bugtracker.py	2011-02-23 20:26:53 +0000
+++ lib/lp/bugs/interfaces/bugtracker.py	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -33,6 +33,7 @@
     export_read_operation,
     export_write_operation,
     exported,
+    operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
     operation_returns_entry,
@@ -390,7 +391,8 @@
 
     @operation_parameters(
         component_group_name=TextLine(
-            title=u"The name of the remote component group", required=True))
+            title=u"The name of the remote component group",
+            required=True))
     @operation_returns_entry(Interface)
     @export_read_operation()
     def getRemoteComponentGroup(component_group_name):
@@ -399,6 +401,23 @@
         :param component_group_name: Name of the component group to retrieve.
         """
 
+    @operation_parameters(
+        distribution=TextLine(
+            title=u"The distribution for the source package",
+            required=True),
+        sourcepackagename=TextLine(
+            title=u"The source package name",
+            required=True))
+    @operation_returns_entry(Interface)
+    @export_read_operation()
+    @operation_for_version('devel')
+    def getRemoteComponentForDistroSourcePackageName(
+        distribution, sourcepackagename):
+        """Returns the component linked to this source package, if any.
+
+        If no components have been linked, returns value of None.
+        """
+
 
 class IBugTrackerSet(Interface):
     """A set of IBugTracker's.
@@ -538,6 +557,10 @@
             title=_('Name'),
             description=_("The name of a software component "
                           "as shown in Launchpad.")))
+    sourcepackagename = Choice(
+        title=_("Package"), required=False, vocabulary='SourcePackageName')
+    distribution = Choice(
+        title=_("Distribution"), required=False, vocabulary='Distribution')
 
     distro_source_package = exported(
         Reference(

=== modified file 'lib/lp/bugs/model/bugtracker.py'
--- lib/lp/bugs/model/bugtracker.py	2011-05-28 04:09:11 +0000
+++ lib/lp/bugs/model/bugtracker.py	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212
@@ -271,10 +271,17 @@
 
         if component_name is None:
             return None
+        elif component_name.isdigit():
+            component_id = int(component_name)
+            return Store.of(self).find(
+                BugTrackerComponent,
+                BugTrackerComponent.id == component_id,
+                BugTrackerComponent.component_group == self.id).one()
         else:
             return Store.of(self).find(
                 BugTrackerComponent,
-                (BugTrackerComponent.name == component_name)).one()
+                BugTrackerComponent.name == component_name,
+                BugTrackerComponent.component_group == self.id).one()
 
     def addCustomComponent(self, component_name):
         """Adds a component locally that isn't synced from a remote tracker
@@ -680,11 +687,31 @@
         """See `IBugTracker`."""
         component_group = None
         store = IStore(BugTrackerComponentGroup)
-        component_group = store.find(
-            BugTrackerComponentGroup,
-            name = component_group_name).one()
+        if component_group_name.isdigit():
+            component_group_id = int(component_group_name)
+            component_group = store.find(
+                BugTrackerComponentGroup,
+                BugTrackerComponentGroup.id == component_group_id).one()
+        else:
+            component_group = store.find(
+                BugTrackerComponentGroup,
+                BugTrackerComponentGroup.name == component_group_name).one()
         return component_group
 
+    def getRemoteComponentForDistroSourcePackageName(
+        self, distribution, sourcepackagename):
+        """See `IBugTracker`."""
+        if distribution is None:
+            return None
+        dsp = distribution.getSourcePackage(sourcepackagename)
+        if dsp is None:
+            return None
+        return Store.of(self).find(
+            BugTrackerComponent,
+            BugTrackerComponent.distribution == distribution.id,
+            BugTrackerComponent.source_package_name ==
+                dsp.sourcepackagename.id).one()
+
 
 class BugTrackerSet:
     """Implements IBugTrackerSet for a container or set of BugTrackers,
@@ -751,7 +778,7 @@
         # Without context, cannot tell what store flavour is desirable.
         store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
         if active is not None:
-            clauses = [BugTracker.active==active]
+            clauses = [BugTracker.active == active]
         else:
             clauses = []
         results = store.find(BugTracker, *clauses)

=== renamed file 'lib/canonical/launchpad/scripts/bzremotecomponentfinder.py' => 'lib/lp/bugs/scripts/bzremotecomponentfinder.py'
--- lib/canonical/launchpad/scripts/bzremotecomponentfinder.py	2010-12-20 03:21:03 +0000
+++ lib/lp/bugs/scripts/bzremotecomponentfinder.py	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Utilities for the update-bugzilla-remote-components cronscript"""
@@ -47,7 +47,7 @@
 
     def __init__(self, base_url=None):
         self.base_url = re.sub(r'/$', '', base_url)
-        self.url = "%s/query.cgi?format=advanced" %(self.base_url)
+        self.url = "%s/query.cgi?format=advanced" % (self.base_url)
         self.products = {}
 
     def getPage(self):
@@ -117,7 +117,6 @@
         self.static_bugzilla_text = static_bugzilla_text
 
     def getRemoteProductsAndComponents(self, bugtracker_name=None):
-        """"""
         lp_bugtrackers = getUtility(IBugTrackerSet)
         if bugtracker_name is not None:
             lp_bugtrackers = [
@@ -133,10 +132,10 @@
             if lp_bugtracker.name in self._BLACKLIST:
                 continue
 
-            self.logger.info("%s: %s" %(
+            self.logger.info("%s: %s" % (
                 lp_bugtracker.name, lp_bugtracker.baseurl))
             bz_bugtracker = BugzillaRemoteComponentScraper(
-                base_url = lp_bugtracker.baseurl)
+                base_url=lp_bugtracker.baseurl)
 
             if self.static_bugzilla_text is not None:
                 self.logger.debug("Using static bugzilla text")
@@ -188,10 +187,10 @@
             # added to launchpad.  Record them for now.
             for component in product['components'].values():
                 components_to_add.append(
-                    "('%s', %d, 'True', 'False')" %(
+                    "('%s', %d, 'True', 'False')" % (
                         component['name'], lp_component_group.id))
 
-        if len(components_to_add)>0:
+        if len(components_to_add) > 0:
             sqltext = """
             INSERT INTO BugTrackerComponent
             (name, component_group, is_visible, is_custom)

=== modified file 'lib/lp/bugs/templates/bugtracker-index.pt'
--- lib/lp/bugs/templates/bugtracker-index.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/bugs/templates/bugtracker-index.pt	2011-06-03 15:12:07 +0000
@@ -39,7 +39,12 @@
         <div tal:replace="structure context/@@+portlet-projects" />
       </div>
     </div>
-    <div class="yui-u" tal:condition="context/watches">
+    <div class="yui-u"
+      tal:condition="features/bugs.bugtracker_components.enabled">
+      <div tal:replace="structure context/@@+portlet-components" />
+    </div>
+    <div class="yui-u"
+      tal:condition="context/watches">
       <div tal:replace="structure context/@@+portlet-watches" />
     </div>
   </div>

=== added file 'lib/lp/bugs/templates/bugtracker-portlet-components.pt'
--- lib/lp/bugs/templates/bugtracker-portlet-components.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtracker-portlet-components.pt	2011-06-03 15:12:07 +0000
@@ -0,0 +1,43 @@
+<div
+    xmlns:tal="http://xml.zope.org/namespaces/tal";
+    xmlns:metal="http://xml.zope.org/namespaces/metal";
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+    class="portlet" id="portlet-components"
+    tal:define="related_component_groups view/related_component_groups">
+  <h2>Components</h2>
+
+  <p>
+    You can link components from this bug tracker to their corresponding
+    distribution source packages in the project's &ldquo;Change
+    components&rdquo; page.
+  </p>
+
+  <p tal:condition="not: related_component_groups/count">
+    <strong>This bug tracker has no components.</strong>
+  </p>
+
+  <dl tal:condition="related_component_groups">
+    <tal:group repeat="component_group related_component_groups">
+      <dt><span tal:replace="component_group/name" />:</dt>
+      <tal:components define="components component_group/components">
+        <dd class="subordinate">
+          <ul style="margin-top: 0" class="horizontal">
+            <li style="white-space: nowrap;" tal:repeat="component components">
+              <span tal:replace="component/name" />
+              <a
+                tal:condition="component/distro_source_package"
+                tal:replace="structure component/distro_source_package/fmt:link" />
+              <a class="menu-link-edit sprite edit"
+                tal:attributes="href string:${context/fmt:url}/+components/${component_group/id}/${component/id}/+edit"><span
+                  class="invisible-link">Edit component</span></a>
+            </li>
+          </ul>
+        </dd>
+        <dd class="subordinate"
+          tal:condition="not: components">
+          This bug tracker has no components for this group.
+        </dd>
+      </tal:components>
+    </tal:group>
+  </dl>
+</div>

=== modified file 'lib/lp/bugs/tests/test_bugtracker_components.py'
--- lib/lp/bugs/tests/test_bugtracker_components.py	2010-10-15 06:01:53 +0000
+++ lib/lp/bugs/tests/test_bugtracker_components.py	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test for components and component groups (products) in bug trackers."""
@@ -7,7 +7,6 @@
 
 __all__ = []
 
-import unittest
 import transaction
 
 from canonical.launchpad.ftests import login_person
@@ -21,12 +20,12 @@
     )
 
 
-class TestBugTrackerComponent(TestCaseWithFactory):
+class BugTrackerComponentTestCase(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestBugTrackerComponent, self).setUp()
+        super(BugTrackerComponentTestCase, self).setUp()
 
         regular_user = self.factory.makePerson()
         login_person(regular_user)
@@ -88,14 +87,15 @@
 
     def test_link_distro_source_package(self):
         """Check that a link can be set to a distro source package"""
-        component = self.factory.makeBugTrackerComponent(
+        example_component = self.factory.makeBugTrackerComponent(
             u'example', self.comp_group)
-        package = self.factory.makeDistributionSourcePackage()
-        self.assertIs(None, component.distro_source_package)
+        dsp = self.factory.makeDistributionSourcePackage(u'example')
 
-        # Set the source package on the component
-        component.distro_source_package = package
-        self.assertIsNot(None, component.distro_source_package)
+        example_component.distro_source_package = dsp
+        self.assertEqual(dsp, example_component.distro_source_package)
+        comp = self.bug_tracker.getRemoteComponentForDistroSourcePackageName(
+            dsp.distribution, dsp.sourcepackagename)
+        self.assertIsNot(example_component, comp)
 
 
 class TestBugTrackerWithComponents(TestCaseWithFactory):
@@ -148,7 +148,7 @@
     def test_multiple_product_bugtracker(self):
         """Bug tracker with multiple products and components"""
         # Create several component groups with varying numbers of components
-        comp_group_i = self.bug_tracker.addRemoteComponentGroup(u'alpha')
+        self.bug_tracker.addRemoteComponentGroup(u'alpha')
 
         comp_group_ii = self.bug_tracker.addRemoteComponentGroup(u'beta')
         comp_group_ii.addComponent(u'example-beta-1')
@@ -291,10 +291,3 @@
         component = ws_object(self.launchpad, db_comp)
         package = ws_object(self.launchpad, db_src_pkg)
         component.distro_source_package = package
-
-
-def test_suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
-
-    return suite

=== modified file 'lib/lp/bugs/tests/test_bzremotecomponentfinder.py'
--- lib/lp/bugs/tests/test_bzremotecomponentfinder.py	2010-12-20 03:21:03 +0000
+++ lib/lp/bugs/tests/test_bzremotecomponentfinder.py	2011-06-03 15:12:07 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests cronscript for retriving components from remote Bugzillas"""
@@ -8,14 +8,13 @@
 __all__ = []
 
 import os
-import unittest
 import transaction
 
 from canonical.testing import DatabaseFunctionalLayer
 from canonical.launchpad.ftests import (
     login,
     )
-from canonical.launchpad.scripts.bzremotecomponentfinder import (
+from lp.bugs.scripts.bzremotecomponentfinder import (
     BugzillaRemoteComponentFinder,
     BugzillaRemoteComponentScraper,
     dictFromCSV,
@@ -105,7 +104,7 @@
 
         # Set up remote bug tracker with synthetic data
         bz_bugtracker = BugzillaRemoteComponentScraper(
-            base_url = "http://bugzilla.example.org";)
+            base_url="http://bugzilla.example.org";)
         bz_bugtracker.products = {
             u'alpha': {
                 'name': u'alpha',
@@ -147,8 +146,8 @@
             title="fdo-example",
             name="fdo-example")
         transaction.commit()
-        bz_bugtracker = BugzillaRemoteComponentScraper(
-            base_url = "http://bugzilla.example.org";)
+        BugzillaRemoteComponentScraper(
+            base_url="http://bugzilla.example.org";)
 
         page_text = read_test_file("bugzilla-fdo-advanced-query.html")
         finder = BugzillaRemoteComponentFinder(
@@ -165,7 +164,8 @@
         self.assertIsNot(None, comp)
         self.assertEqual(u'Driver/Radeon', comp.name)
 
-# FIXME: This takes ~9 sec to run, but mars says new testsuites need to compete in 2
+# FIXME: This takes ~9 sec to run, but mars says new testsuites need to
+#        compete in 2
 #    def test_cronjob(self):
 #        """Runs the cron job to verify it executes without error"""
 #        import subprocess
@@ -182,9 +182,3 @@
 #        self.assertTrue('ERROR' not in err)
 #        self.assertTrue('CRITICAL' not in err)
 #        self.assertTrue('Exception raised' not in err)
-
-def test_suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
-
-    return suite

=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py	2011-05-25 07:21:58 +0000
+++ lib/lp/services/features/flags.py	2011-06-03 15:12:07 +0000
@@ -38,6 +38,10 @@
 # NOTE: "default behaviour" does not specify a default value.  It
 # merely documents the code's behaviour if no value is specified.
 flag_info = sorted([
+    ('bugs.bugtracker_components.enabled',
+     'boolean',
+     ('Enables the display of bugtracker components.'),
+     ''),
     ('code.branchmergequeue',
      'boolean',
      'Enables merge queue pages and lists them on branch pages.',


Follow ups