← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bryce/launchpad/lp-617698-forwarding into lp:launchpad

 

Bryce Harrington has proposed merging lp:~bryce/launchpad/lp-617698-forwarding into lp:launchpad with lp:~bryce/launchpad/lp-617695-linkui as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #617698 When forwarding a bug to an external bug tracker, the component field doesn't get filled in
  https://bugs.launchpad.net/bugs/617698


Implement remote_component support when forwarding bugs.

This branch is dependent on the lp:~bryce/launchpad/lp-617695-linkui so that branch should be landed first.

This causes the 'component' field to be properly filled in for the bugzilla submission page when forwarding a bug report to a remote project.

lint has been checked.  ec2 test has been run and passed successfully.



-- 
https://code.launchpad.net/~bryce/launchpad/lp-617698-forwarding/+merge/41003
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bryce/launchpad/lp-617698-forwarding into lp:launchpad.
=== modified file 'lib/canonical/launchpad/scripts/bzremotecomponentfinder.py'
--- lib/canonical/launchpad/scripts/bzremotecomponentfinder.py	2010-10-20 00:24:50 +0000
+++ lib/canonical/launchpad/scripts/bzremotecomponentfinder.py	2010-11-16 19:54:52 +0000
@@ -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 = [

=== modified file 'lib/canonical/widgets/bugtask.py'
--- lib/canonical/widgets/bugtask.py	2010-11-08 12:52:43 +0000
+++ lib/canonical/widgets/bugtask.py	2010-11-16 19:54:52 +0000
@@ -495,6 +495,25 @@
         return distribution
 
 
+class UbuntuSourcePackageNameWidget(
+    BugTaskSourcePackageNameWidget):
+    """Package widget where the distribution can be assumed as Ubuntu
+
+    This widgets works the same as `BugTaskSourcePackageNameWidget`,
+    except that it assumes the distribution is 'ubuntu'.
+    """
+    distribution_name = "ubuntu"
+
+    def getDistribution(self):
+        """See `BugTaskSourcePackageNameWidget`"""
+        distribution = getUtility(IDistributionSet).getByName(
+            self.distribution_name)
+        if distribution is None:
+            raise UnexpectedFormData(
+                "No such distribution: %s" % self.distribution_name)
+        return distribution
+
+
 class AssigneeDisplayWidget(BrowserWidget):
     """A widget for displaying an assignee."""
 

=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
--- lib/lp/bugs/browser/bugalsoaffects.py	2010-09-03 03:12:39 +0000
+++ lib/lp/bugs/browser/bugalsoaffects.py	2010-11-16 19:54:52 +0000
@@ -664,8 +664,20 @@
             title = bug.title
             description = u"Originally reported at:\n  %s\n\n%s" % (
                 canonical_url(bug), bug.description)
+            remote_component = ""
+            comp_group = target.bugtracker.getRemoteComponentGroup(
+                target.remote_product)
+
+            # Look up the remote component if we have the necessary info
+            if comp_group is not None and data.get('add_packaging', False):
+                package_name = self.context.target.sourcepackagename
+                for component in comp_group.components:
+                    if (component.distro_source_package is not None and
+                        component.distro_source_package.name == package_name):
+                        remote_component = component.name
+
             return target.bugtracker.getBugFilingAndSearchLinks(
-                target.remote_product, title, description)
+                target.remote_product, remote_component, title, description)
 
 
 class BugTrackerCreationStep(AlsoAffectsStep):

=== modified file 'lib/lp/bugs/browser/bugtracker.py'
--- lib/lp/bugs/browser/bugtracker.py	2010-10-15 08:23:19 +0000
+++ lib/lp/bugs/browser/bugtracker.py	2010-11-16 19:54:52 +0000
@@ -10,6 +10,7 @@
     'BugTrackerBreadcrumb',
     'BugTrackerComponentGroupNavigation',
     'BugTrackerEditView',
+    'BugTrackerEditComponentView',
     'BugTrackerNavigation',
     'BugTrackerNavigationMenu',
     'BugTrackerSetBreadcrumb',
@@ -21,6 +22,7 @@
     ]
 
 from itertools import chain
+from storm.locals import Store
 
 from zope.app.form.browser import TextAreaWidget
 from zope.component import getUtility
@@ -67,13 +69,19 @@
     DelimitedListWidget,
     LaunchpadRadioWidget,
     )
+from canonical.widgets.bugtask import (
+    UbuntuSourcePackageNameWidget,
+    )
 from lp.bugs.interfaces.bugtracker import (
     BugTrackerType,
     IBugTracker,
     IBugTrackerSet,
     IRemoteBug,
+    IBugTrackerComponent,
     IBugTrackerComponentGroup,
     )
+from lp.bugs.model.bugtracker import BugTrackerComponent
+from lp.registry.interfaces.distribution import IDistributionSet
 from lp.services.propertycache import cachedproperty
 
 # A set of bug tracker types for which there can only ever be one bug
@@ -229,6 +237,14 @@
         return shortlist(chain(self.context.projects,
                                self.context.products), 100)
 
+    @property
+    def related_component_groups(self):
+        """Return all component groups and components
+
+        This property was created for the Related components portlet in
+        the bug tracker's page.
+        """
+        return self.context.getAllRemoteComponentGroups()
 
 BUG_TRACKER_ACTIVE_VOCABULARY = SimpleVocabulary.fromItems(
     [('On', True), ('Off', False)])
@@ -447,16 +463,113 @@
             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)
+
+    @property
+    def page_title(self):
+        return smartquote(
+            u'Link a distribution source package to the %s component'
+            % self.context.name)
+
+    @property
+    def field_names(self):
+        field_names = [
+            'sourcepackagename',
+            ]
+        return field_names
+
+    def setUpWidgets(self, context=None):
+        for field in self.form_fields:
+            if (field.custom_widget is None and
+                field.__name__ in self.custom_widgets):
+                field.custom_widget = self.custom_widgets[field.__name__]
+        self.widgets = form.setUpWidgets(
+            self.form_fields, self.prefix, self.context, self.request,
+            data=self.initial_values, adapters=self.adapters,
+            ignore_request=False)
+
+    @property
+    def initial_values(self):
+        """See `LaunchpadFormView.`"""
+        field_values = {}
+        for name in self.field_names:
+            if name == 'sourcepackagename':
+                pkg = self.context.distro_source_package
+                if pkg is not None:
+                    field_values['sourcepackagename'] = pkg.name
+                else:
+                    field_values['sourcepackagename'] = ""
+            else:
+                field_values[name] = getattr(self.context, name)
+
+        return field_values
+
+    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.
+        """
+        if context is None:
+            context = self.context
+        component = context
+
+        sourcepackagename = self.request.form.get(
+            self.widgets['sourcepackagename'].name)
+
+        distro_name = self.widgets['sourcepackagename'].distribution_name
+        distribution = getUtility(IDistributionSet).getByName(distro_name)
+        pkg = distribution.getSourcePackage(sourcepackagename)
+
+        # Has this source package already been assigned to a component?
+        pkgs = Store.of(component).find(
+            BugTrackerComponent,
+            BugTrackerComponent.distribution == distribution.id,
+            BugTrackerComponent.source_package_name ==
+            pkg.sourcepackagename.id)
+        if pkgs.count()>0:
+            self.request.response.addNotification(
+                "The %s source package is already linked to %s:%s in %s" %(
+                    sourcepackagename, component.component_group.name,
+                    component.name, distro_name))
+            return
+
+        component.distro_source_package = pkg
+        self.request.response.addNotification(
+            "%s:%s is now linked to the %s source package in %s" %(
+                component.component_group.name, component.name,
+                sourcepackagename, distro_name))
+
+    @action('Save Changes', name='save')
+    def save_action(self, action, data):
+        """Update the component with the form data."""
+        self.updateContextFromData(data)
+
+        self.next_url = canonical_url(
+            self.context.component_group.bug_tracker)
 
 
 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	2010-10-28 09:11:36 +0000
+++ lib/lp/bugs/browser/configure.zcml	2010-11-16 19:54:52 +0000
@@ -811,6 +811,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="../templates/bugtracker-edit-component.pt"/>
         <browser:pages
             for="lp.bugs.interfaces.bugtracker.IBugTracker"
             class="lp.bugs.browser.bugtracker.BugTrackerView"
@@ -822,6 +828,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

=== modified file 'lib/lp/bugs/browser/tests/bugtask-adding-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-adding-views.txt	2010-11-01 14:58:34 +0000
+++ lib/lp/bugs/browser/tests/bugtask-adding-views.txt	2010-11-16 19:54:52 +0000
@@ -615,7 +615,7 @@
 
     >>> print_links(add_task_view.upstream_bugtracker_links)
     bug_filing_url:
-    ...?product=foo&short_desc=Reflow%20...&long_desc=Originally%20...
+    ...?product=foo...&short_desc=Reflow%20...&long_desc=Originally%20...
     bug_search_url: ...query.cgi?product=foo&short_desc=Reflow%20problems...
 
 If the product's `bugtracker` isn't specified its

=== 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	2010-11-16 19:54:52 +0000
@@ -0,0 +1,116 @@
+# Copyright 2010 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
+
+import unittest
+from zope.component import getUtility
+
+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 TestBugTrackerEditComponentView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBugTrackerEditComponentView, 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 the Example component'
+        self.assertEqual(label, view.page_title)
+        fields = ['sourcepackagename']
+        self.assertEqual(fields, view.field_names)
+
+    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(1, len(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_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 = (
+            u"""The example source package is already linked to """
+            """alpha:b in ubuntu""")
+        self.assertEqual(expected, notifications.pop().message)
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
+
+    return suite
+
+if __name__ == '__main__':
+    unittest.TextTestRunner().run(test_suite())

=== modified file 'lib/lp/bugs/doc/bugtracker.txt'
--- lib/lp/bugs/doc/bugtracker.txt	2010-10-27 20:00:54 +0000
+++ lib/lp/bugs/doc/bugtracker.txt	2010-11-16 19:54:52 +0000
@@ -356,26 +356,26 @@
 Filing a bug on the remote tracker
 ----------------------------------
 
-The IBugTracker interface defines a method, getBugFilingAndSearchLinks(),
-which returns the URLs of the bug filing form and the bug search on
-the remote bug tracker as a dict. It accepts three parameters:
-remote_product, which is the name of the product on the remote
-tracker; summary, which is the bug summary to be passed to the remote
-bug tracker for searching or filing and description, which is the full
-description of the bug. This is only passed to the bug filing form as
-it is too specific for the search form.
+The IBugTracker interface defines a method,
+getBugFilingAndSearchLinks(), which returns the URLs of the bug filing
+form and the bug search on the remote bug tracker as a dict. It accepts
+fource parameters: remote_product, which is the name of the product on
+the remote tracker; remote_component, which corresponds to the component
+field; summary, which is the bug summary to be passed to the remote bug
+tracker for searching or filing and description, which is the full
+description of the bug. This is only passed to the bug filing form as it
+is too specific for the search form.
 
     >>> def print_links(links_dict):
     ...     for key in sorted(links_dict):
     ...         print "%s: %s" % (key, links_dict[key])
 
     >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks(
-    ...     remote_product='testproduct', summary="Foo", description="Bar")
+    ...     remote_product='testproduct', remote_component='testcomponent',
+    ...     summary="Foo", description="Bar")
     >>> print_links(links)
-    bug_filing_url:
-    https://.../enter_bug.cgi?product=testproduct&short_desc=Foo&long_desc=Bar
-    bug_search_url:
-    https://.../query.cgi?product=testproduct&short_desc=Foo
+    bug_filing_url: https://.../enter_bug.cgi?product=testproduct&component=testcomponent&short_desc=Foo&long_desc=Bar
+    bug_search_url: https://.../query.cgi?product=testproduct&short_desc=Foo
 
 For the RT tracker we specify a Queue in which to file a ticket.
 
@@ -462,7 +462,8 @@
     >>> example_roundup = factory.makeBugTracker(
     ...     'http://roundup.example.com', BugTrackerType.ROUNDUP)
     >>> links = example_roundup.getBugFilingAndSearchLinks(
-    ...     remote_product='testproduct', summary="Foo", description="Bar")
+    ...     remote_product='testproduct', remote_component='testcomponent',
+    ...     summary="Foo", description="Bar")
     >>> print_links(links)
     bug_filing_url: http://.../issue?@template=item&title=Foo&@note=Bar
     bug_search_url: http://.../issue?@template=search&@search_text=Foo
@@ -505,7 +506,7 @@
     >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks(
     ...     'test', None, None)
     >>> print_links(links)
-    bug_filing_url: ...?product=test&short_desc=&long_desc=
+    bug_filing_url: ...?product=test&component=&short_desc=&long_desc=
     bug_search_url: ...?product=test&short_desc=
 
 The remote_product, summary and description values are URL-encoded to ensure
@@ -514,7 +515,7 @@
     >>> links = mozilla_bugzilla.getBugFilingAndSearchLinks(
     ...     remote_product='@test&', summary="%&", description="()")
     >>> print_links(links)
-    bug_filing_url: ...?product=%40test%26&short_desc=%25%26&long_desc=%28%29
+    bug_filing_url: ...?product=%40test%26&component=&short_desc=%25%26&long_desc=%28%29
     bug_search_url: ...?product=%40test%26&short_desc=%25%26
 
 getBugFilingAndSearchLinks() will also handle unicode values in the
@@ -583,8 +584,6 @@
     ...     remote_product='testproduct', summary="Foo", description="Bar")
 
     >>> print_links(links)
-    bug_filing_url:
-    http://.../enter_bug.cgi?product=testproduct&short_desc=Foo&comment=Bar
-    bug_search_url:
-    http://.../query.cgi?product=testproduct&short_desc=Foo
+    bug_filing_url: http://.../enter_bug.cgi?product=testproduct&component=&short_desc=Foo&comment=Bar
+    bug_search_url: http://.../query.cgi?product=testproduct&short_desc=Foo
 

=== modified file 'lib/lp/bugs/interfaces/bugtracker.py'
--- lib/lp/bugs/interfaces/bugtracker.py	2010-11-04 02:32:16 +0000
+++ lib/lp/bugs/interfaces/bugtracker.py	2010-11-16 19:54:52 +0000
@@ -306,12 +306,14 @@
     watches_needing_update = Attribute(
         "The set of bug watches that need updating.")
 
-    def getBugFilingAndSearchLinks(remote_product, summary=None,
-                                   description=None):
+    def getBugFilingAndSearchLinks(remote_product, remote_component=None,
+                                   summary=None, description=None):
         """Return the bug filing and search links for the tracker.
 
         :param remote_product: The name of the product on which the bug
             is to be filed or search for.
+        :param remote_product: The name of the component on which the bug
+            is to be filed or search for.
         :param summary: The string with which to pre-filly the summary
             field of the upstream bug tracker's search and bug filing forms.
         :param description: The string with which to pre-filly the description
@@ -538,6 +540,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	2010-11-04 02:32:16 +0000
+++ lib/lp/bugs/model/bugtracker.py	2010-11-16 19:54:52 +0000
@@ -249,7 +249,6 @@
 
     def addComponent(self, component_name):
         """Adds a component that is synced from a remote bug tracker"""
-
         component = BugTrackerComponent()
         component.name = component_name
         component.component_group = self
@@ -266,18 +265,23 @@
         None is returned if there is no component by that name in the
         group.
         """
-
         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
         """
-
         component = BugTrackerComponent()
         component.name = component_name
         component.component_group = self
@@ -327,6 +331,7 @@
     _filing_url_patterns = {
         BugTrackerType.BUGZILLA: (
             "%(base_url)s/enter_bug.cgi?product=%(remote_product)s"
+            "&component=%(remote_component)s"
             "&short_desc=%(summary)s&long_desc=%(description)s"),
         BugTrackerType.GOOGLE_CODE: (
             "%(base_url)s/entry?summary=%(summary)s&"
@@ -385,6 +390,7 @@
         return {
             gnome_bugzilla: (
                 "%(base_url)s/enter_bug.cgi?product=%(remote_product)s"
+                "&component=%(remote_component)s"
                 "&short_desc=%(summary)s&comment=%(description)s"),
             }
 
@@ -401,7 +407,8 @@
         else:
             return False
 
-    def getBugFilingAndSearchLinks(self, remote_product, summary=None,
+    def getBugFilingAndSearchLinks(self, remote_product,
+                                   remote_component=None, summary=None,
                                    description=None):
         """See `IBugTracker`."""
         bugtracker_urls = {'bug_filing_url': None, 'bug_search_url': None}
@@ -416,6 +423,10 @@
             # quote() doesn't blow up later on.
             remote_product = ''
 
+        if remote_component is None:
+            # Ditto for remote component.
+            remote_component = ''
+
         if self in self._custom_filing_url_patterns:
             # Some bugtrackers are customised to accept different
             # querystring parameters from the default. We special-case
@@ -473,6 +484,7 @@
             url_components = {
                 'base_url': base_url,
                 'remote_product': quote(remote_product),
+                'remote_component': quote(remote_component),
                 'summary': quote(summary),
                 'description': quote(description),
                 }
@@ -673,9 +685,17 @@
         """See `IBugTracker`."""
         component_group = None
         store = IStore(BugTrackerComponentGroup)
-        component_group = store.find(
-            BugTrackerComponentGroup,
-            name = component_group_name).one()
+        if component_group_name is None:
+            return None
+        elif component_group_name.isdigit():
+            component_group_id = int(component_group_name)
+            component_group = store.find(
+                BugTrackerComponentGroup,
+                id = component_group_id).one()
+        else:
+            component_group = store.find(
+                BugTrackerComponentGroup,
+                name = component_group_name).one()
         return component_group
 
 

=== added file 'lib/lp/bugs/templates/bugtracker-edit-component.pt'
--- lib/lp/bugs/templates/bugtracker-edit-component.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtracker-edit-component.pt	2010-11-16 19:54:52 +0000
@@ -0,0 +1,16 @@
+<bug-tracker-edit-component
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="malone">
+
+  <div metal:fill-slot="main">
+    <h1>Link <span tal:replace="context/component_group/name"/> component '<span tal:replace="context/name"/>'</h1>
+    <div metal:use-macro="context/@@launchpad_form/form">
+      <p>Configure component</p>
+    </div>
+  </div>
+
+</bug-tracker-edit-component>

=== modified file 'lib/lp/bugs/templates/bugtracker-index.pt'
--- lib/lp/bugs/templates/bugtracker-index.pt	2009-09-01 15:58:46 +0000
+++ lib/lp/bugs/templates/bugtracker-index.pt	2010-11-16 19:54:52 +0000
@@ -34,6 +34,9 @@
     <div class="yui-g">
       <div class="first yui-u">
         <div tal:replace="structure context/@@+portlet-details" />
+        <div tal:condition="features/bugtracker_components">
+          <div tal:replace="structure context/@@+portlet-components" />
+        </div>
       </div>
       <div class="yui-u">
         <div tal:replace="structure context/@@+portlet-projects" />

=== 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	2010-11-16 19:54:52 +0000
@@ -0,0 +1,37 @@
+<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">
+  <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>
+  <ul tal:define="related_component_groups view/related_component_groups">
+    <li tal:repeat="component_group related_component_groups">
+      <strong><span tal:replace="component_group/name" /></strong>
+      <ul tal:define="components component_group/components">
+        <li tal:repeat="component components">
+          <span tal:replace="component/name" />
+          &nbsp;
+          <span tal:condition="component/distro_source_package">
+            <a class="sprite edit"
+               tal:attributes="href string:${context/fmt:url}/+components/${component_group/id}/${component/id}/+edit">
+               <span tal:replace="structure component/distro_source_package/name"/></a>
+          </span>
+          <a class="sprite add"
+             tal:condition="not: component/distro_source_package"
+             tal:attributes="href string:${context/fmt:url}/+components/${component_group/id}/${component/id}/+edit"></a>
+        </li>
+        <li tal:condition="not: components">
+          <i>This bug tracker has no components for this group</i>
+        </li>
+      </ul>
+    </li>
+    <li tal:condition="not: related_component_groups">
+      <i>This bug tracker has no components</i>
+    </li>
+  </ul>
+</div>


Follow ups