← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/bug-643538-code into lp:launchpad/devel

 

Brad Crittenden has proposed merging lp:~bac/launchpad/bug-643538-code into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #634538 Python-setuptools patch for gkeyring
  https://bugs.launchpad.net/bugs/634538
  #643538 Launchpad must state the project's code usage
  https://bugs.launchpad.net/bugs/643538


= Summary =

Launchpad must state the project's code usage.  The product +code-index page needs to clearly state if the project is using Launchpad.  If it isn't it should refer to where it is hosted.  

== Proposed fix ==

Conditionally present the correct message.  Also moved some of the code summary information into portlets in order to be more like the other application areas.

== Pre-implementation notes ==

Chats with Curtis, Edwin, and Jon.

== Implementation details ==

As above.

== Tests ==

Basically all of the code tests need to run:
bin/test -vvm lp.code

== Demo and Q/A ==

Create a new project and visit:
https://code.launchpad.dev/mynewproject

= Launchpad lint =

The lint issues are pretty much intractable.  I cleaned up a lot.

Linting changed files:
  lib/lp/code/browser/configure.zcml
  lib/canonical/launchpad/icing/style.css
  lib/lp/code/templates/product-branches.pt
  lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt
  lib/lp/code/browser/branch.py
  lib/lp/code/browser/tests/test_product.py
  lib/lp/registry/browser/product.py
  lib/lp/code/browser/tests/test_branch.py
  lib/lp/code/stories/branches/xx-product-branches.txt
  lib/lp/code/templates/product-portlet-codestatistics-content.pt
  lib/lp/registry/browser/pillar.py
  lib/lp/code/stories/branches/xx-person-branches.txt
  lib/lp/code/templates/product-portlet-codestatistics.pt
  lib/lp/code/templates/product-branch-summary.pt
  lib/lp/registry/browser/tests/pillar-views.txt
  lib/lp/code/stories/branches/xx-private-branch-listings.txt
  lib/lp/registry/model/product.py
  lib/lp/registry/tests/test_service_usage.py
  lib/lp/code/browser/branchlisting.py

./lib/lp/code/stories/branches/xx-private-branch-listings.txt
      83: want exceeds 78 characters.
      93: want exceeds 78 characters.
./lib/lp/code/browser/branchlisting.py
     409: Line exceeds 78 characters.

-- 
https://code.launchpad.net/~bac/launchpad/bug-643538-code/+merge/36377
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-643538-code into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/icing/style.css'
--- lib/canonical/launchpad/icing/style.css	2010-05-28 19:47:23 +0000
+++ lib/canonical/launchpad/icing/style.css	2010-09-22 20:23:46 +0000
@@ -550,6 +550,16 @@
     color: black !important;
 }
 
+.code-links td.code-count {
+  text-align: right;
+  padding-right: 0.5em;
+}
+
+.code-links td.code-link {
+  text-align: left;
+  margin: 0 0 0 0;
+}
+
 
 /* === Bugs === */
 /* The Launchpad Bugs application uses a maroon color: */
@@ -585,6 +595,15 @@
   padding-right: 1em;
 }
 
+.bug-links td.bugs-count {
+  text-align: right;
+  padding-right: 0.5em;
+}
+
+.bug-links td.bugs-link {
+  text-align: left;
+}
+
 /* --- Blueprints --- */
 
 body.tab-specifications #actions, body.tab-specifications .results {
@@ -652,15 +671,6 @@
   margin: 0.5em;
 }
 
-.bug-links td.bugs-count {
-  text-align: right;
-  padding-right: 0.5em;
-}
-
-.bug-links td.bugs-link {
-  text-align: left;
-}
-
 /* ====== Content area styles ====== */
 
 /* -- Front pages -- */

=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2010-08-24 10:45:57 +0000
+++ lib/lp/code/browser/branch.py	2010-09-22 20:23:46 +0000
@@ -16,6 +16,7 @@
     'BranchReviewerEditView',
     'BranchMergeQueueView',
     'BranchMirrorStatusView',
+    'BranchMirrorMixin',
     'BranchNameValidationMixin',
     'BranchNavigation',
     'BranchEditMenu',
@@ -374,7 +375,39 @@
         return Link('+new-recipe', text, enabled=enabled, icon='add')
 
 
-class BranchView(LaunchpadView, FeedsMixin):
+class BranchMirrorMixin:
+    """Provide mirror_location property.
+
+    Requires self.branch to be set by the class using this mixin.
+    """
+
+    @property
+    def mirror_location(self):
+        """Check the mirror location to see if it is a private one."""
+        branch = self.branch
+
+        # If the user has edit permissions, then show the actual location.
+        if check_permission('launchpad.Edit', branch):
+            return branch.url
+
+        # XXX: Tim Penhey, 2008-05-30
+        # Instead of a configuration hack we should support the users
+        # specifying whether or not they want the mirror location
+        # hidden or not.  Given that this is a database patch,
+        # it isn't going to happen today.
+        # See bug 235916
+        hosts = config.codehosting.private_mirror_hosts.split(',')
+        private_mirror_hosts = [name.strip() for name in hosts]
+
+        uri = URI(branch.url)
+        for private_host in private_mirror_hosts:
+            if uri.underDomain(private_host):
+                return '<private server>'
+
+        return branch.url
+
+
+class BranchView(LaunchpadView, FeedsMixin, BranchMirrorMixin):
 
     feed_types = (
         BranchFeedLink,
@@ -387,6 +420,7 @@
     label = page_title
 
     def initialize(self):
+        self.branch = self.context
         self.notices = []
         # Replace our context with a decorated branch, if it is not already
         # decorated.
@@ -586,31 +620,6 @@
         return url.startswith("http")
 
     @property
-    def mirror_location(self):
-        """Check the mirror location to see if it is a private one."""
-        branch = self.context
-
-        # If the user has edit permissions, then show the actual location.
-        if check_permission('launchpad.Edit', branch):
-            return branch.url
-
-        # XXX: Tim Penhey, 2008-05-30
-        # Instead of a configuration hack we should support the users
-        # specifying whether or not they want the mirror location
-        # hidden or not.  Given that this is a database patch,
-        # it isn't going to happen today.
-        # See bug 235916
-        hosts = config.codehosting.private_mirror_hosts.split(',')
-        private_mirror_hosts = [name.strip() for name in hosts]
-
-        uri = URI(branch.url)
-        for private_host in private_mirror_hosts:
-            if uri.underDomain(private_host):
-                return '<private server>'
-
-        return branch.url
-
-    @property
     def show_merge_links(self):
         """Return whether or not merge proposal links should be shown.
 

=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py	2010-08-31 11:11:09 +0000
+++ lib/lp/code/browser/branchlisting.py	2010-09-22 20:23:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Base class view for branch listings."""
@@ -82,6 +82,7 @@
 from canonical.launchpad.webapp.batching import TableBatchNavigator
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.launchpad.webapp.publisher import LaunchpadView
+from canonical.launchpad.webapp.tales import MenuAPI
 from canonical.widgets import LaunchpadDropdownWidget
 from lp.blueprints.interfaces.specificationbranch import (
     ISpecificationBranchSet,
@@ -92,6 +93,7 @@
     PersonActiveReviewsView,
     PersonProductActiveReviewsView,
     )
+from lp.code.browser.summary import BranchCountSummaryView
 from lp.code.enums import (
     BranchLifecycleStatus,
     BranchLifecycleStatusFilter,
@@ -112,6 +114,7 @@
 from lp.code.interfaces.seriessourcepackagebranch import (
     IFindOfficialBranchLinks,
     )
+from lp.code.browser.branch import BranchMirrorMixin
 from lp.registry.browser.product import (
     ProductDownloadFileMixin,
     SortSeriesMixin,
@@ -124,7 +127,6 @@
     IPersonProduct,
     IPersonProductFactory,
     )
-from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
@@ -421,7 +423,7 @@
         return sorted(links, key=attrgetter('pocket'))
 
     def getDistroDevelSeries(self, distribution):
-        """Since distribution.currentseries hits the DB every time, cache it."""
+        """distribution.currentseries hits the DB every time so cache it."""
         self._distro_series_map = {}
         try:
             return self._distro_series_map[distribution]
@@ -777,7 +779,7 @@
     """A branch listing that has no associated product or person."""
 
     field_names = ['lifecycle']
-    no_sort_by = (BranchListingSort.DEFAULT,)
+    no_sort_by = (BranchListingSort.DEFAULT, )
 
     no_branch_message = (
         'There are no branches that match the current status filter.')
@@ -932,8 +934,8 @@
     def active_reviews(self):
         text = get_plural_text(
             self.active_review_count,
-            'active review or unmerged proposal',
-            'active reviews or unmerged proposals')
+            'active review',
+            'active reviews')
         return Link('+activereviews', text)
 
     def addbranch(self):
@@ -1022,7 +1024,7 @@
 
     page_title = _('Subscribed')
     label_template = 'Bazaar branches subscribed to by %(displayname)s'
-    no_sort_by = (BranchListingSort.DEFAULT,)
+    no_sort_by = (BranchListingSort.DEFAULT, )
 
     def _getCollection(self):
         return getUtility(IAllBranches).subscribedBy(self.context)
@@ -1122,8 +1124,8 @@
     def active_reviews(self):
         text = get_plural_text(
             self.active_review_count,
-            'active review or unmerged proposal',
-            'active reviews or unmerged proposals')
+            'active review',
+            'active reviews')
         return Link('+activereviews', text, site='code')
 
     @enabled_with_permission('launchpad.Commercial')
@@ -1140,7 +1142,7 @@
     """A base class for product branch listings."""
 
     show_series_links = True
-    no_sort_by = (BranchListingSort.PRODUCT,)
+    no_sort_by = (BranchListingSort.PRODUCT, )
     label_template = 'Bazaar branches of %(displayname)s'
 
     def _getCollection(self):
@@ -1180,8 +1182,13 @@
         return message % self.context.displayname
 
 
+class ProductBranchStatisticsView(BranchCountSummaryView,
+                                  ProductBranchListingView):
+    """Portlet containing branch statistics."""
+
+
 class ProductCodeIndexView(ProductBranchListingView, SortSeriesMixin,
-                           ProductDownloadFileMixin):
+                           ProductDownloadFileMixin, BranchMirrorMixin):
     """Initial view for products on the code virtual host."""
 
     show_set_development_focus = True
@@ -1189,6 +1196,7 @@
     def initialize(self):
         ProductBranchListingView.initialize(self)
         self.product = self.context
+        self.branch = self.development_focus_branch
         revision_cache = getUtility(IRevisionCache)
         self.revision_cache = revision_cache.inProduct(self.product)
 
@@ -1240,6 +1248,7 @@
         # skip subsequent series where the lifecycle status is Merged or
         # Abandoned
         sorted_series = self.sorted_active_series_list
+
         def show_branch(branch):
             if self.selected_lifecycle_status is None:
                 return True
@@ -1323,6 +1332,14 @@
     def committer_text(self):
         return get_plural_text(self.committer_count, _('person'), _('people'))
 
+    @property
+    def configure_codehosting(self):
+        """Get the menu link for configuring code hosting."""
+        series_menu = MenuAPI(self.context.development_focus).overview
+        set_branch = series_menu['set_branch']
+        set_branch.text = 'Configure code hosting'
+        return set_branch
+
 
 class ProductBranchesView(ProductBranchListingView):
     """View for branch listing for a product."""
@@ -1353,7 +1370,7 @@
 class ProjectBranchesView(BranchListingView):
     """View for branch listing for a project."""
 
-    no_sort_by = (BranchListingSort.DEFAULT,)
+    no_sort_by = (BranchListingSort.DEFAULT, )
     extra_columns = ('author', 'product')
     label_template = 'Bazaar branches of %(displayname)s'
     show_series_links = True

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2010-09-22 18:37:57 +0000
+++ lib/lp/code/browser/configure.zcml	2010-09-22 20:23:46 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -961,6 +961,18 @@
         permission="zope.Public"
         name="+code-index"
         template="../templates/product-branches.pt"/>
+   <browser:page
+      for="lp.registry.interfaces.product.IProduct"
+      class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
+      permission="zope.Public"
+      name="+portlet-product-codestatistics"
+      template="../templates/product-portlet-codestatistics.pt"/>
+   <browser:page
+      for="lp.registry.interfaces.product.IProduct"
+      class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
+      permission="zope.Public"
+      name="+portlet-product-codestatistics-content"
+      template="../templates/product-portlet-codestatistics-content.pt"/>
     <browser:page
         for="lp.registry.interfaces.product.IProduct"
         layer="lp.code.publisher.CodeLayer"

=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/browser/tests/test_branch.py	2010-09-22 20:23:46 +0000
@@ -45,7 +45,6 @@
     )
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.testing import (
-    ANONYMOUS,
     login,
     login_person,
     logout,
@@ -78,6 +77,7 @@
             branch_type=BranchType.MIRRORED,
             url="http://example.com/good/mirror";)
         view = BranchView(branch, LaunchpadTestRequest())
+        view.initialize()
         self.assertTrue(view.user is None)
         self.assertEqual(
             "http://example.com/good/mirror";, view.mirror_location)
@@ -89,6 +89,7 @@
             branch_type=BranchType.MIRRORED,
             url="http://private.example.com/bzr-mysql/mysql-5.0";)
         view = BranchView(branch, LaunchpadTestRequest())
+        view.initialize()
         self.assertTrue(view.user is None)
         self.assertEqual(
             "<private server>", view.mirror_location)
@@ -106,6 +107,7 @@
         logout()
         login('eric@xxxxxxxxxxx')
         view = BranchView(branch, LaunchpadTestRequest())
+        view.initialize()
         self.assertEqual(view.user, owner)
         self.assertEqual(
             "http://private.example.com/bzr-mysql/mysql-5.0";,
@@ -126,6 +128,7 @@
         logout()
         login('other@xxxxxxxxxxx')
         view = BranchView(branch, LaunchpadTestRequest())
+        view.initialize()
         self.assertEqual(view.user, other)
         self.assertEqual(
             "<private server>", view.mirror_location)
@@ -160,7 +163,7 @@
             len(branch.mirror_status_message)
             <= branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH,
             "branch.mirror_status_message longer than expected: %r"
-            % (branch.mirror_status_message,))
+            % (branch.mirror_status_message, ))
         self.assertEqual(
             branch.mirror_status_message, branch_view.mirror_status_message)
         self.assertEqual(
@@ -185,7 +188,7 @@
                 'whiteboard': '',
                 'owner': arbitrary_person,
                 'author': arbitrary_person,
-                'product': arbitrary_product
+                'product': arbitrary_product,
                 }
             add_view.add_action.success(data)
             # Make sure that next_mirror_time is a datetime, not an sqlbuilder

=== modified file 'lib/lp/code/browser/tests/test_product.py'
--- lib/lp/code/browser/tests/test_product.py	2010-08-24 02:16:53 +0000
+++ lib/lp/code/browser/tests/test_product.py	2010-09-22 20:23:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the product view classes and templates."""
@@ -10,20 +10,25 @@
     timedelta,
     )
 import unittest
-
 from mechanize import LinkNotFoundError
 import pytz
-from zope.component import (
-    getMultiAdapter,
-    getUtility,
+from zope.component import getUtility
+
+from canonical.launchpad.testing.pages import (
+    extract_text,
+    find_tag_by_id,
     )
-
 from canonical.launchpad.webapp import canonical_url
-from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing import DatabaseFunctionalLayer
+from lp.app.enums import ServiceUsage
+from lp.code.enums import (
+    BranchType,
+    BranchVisibilityRule,
+    )
 from lp.code.interfaces.revision import IRevisionSet
 from lp.testing import (
     ANONYMOUS,
+    BrowserTestCase,
     login,
     login_person,
     TestCaseWithFactory,
@@ -32,9 +37,8 @@
 from lp.testing.views import create_initialized_view
 
 
-class TestProductCodeIndexView(TestCaseWithFactory):
-    """Tests for the product code home page."""
-
+class ProductTestBase(TestCaseWithFactory):
+    """Common methods for tests herein."""
     layer = DatabaseFunctionalLayer
 
     def makeProductAndDevelopmentFocusBranch(self, **branch_args):
@@ -49,6 +53,10 @@
         product.development_focus.branch = branch
         return product, branch
 
+
+class TestProductCodeIndexView(ProductTestBase):
+    """Tests for the product code home page."""
+
     def getBranchSummaryBrowseLinkForProduct(self, product):
         """Get the 'browse code' link from the product's code home.
 
@@ -98,13 +106,15 @@
 
     def test_initial_branches_contains_dev_focus_branch(self):
         product, branch = self.makeProductAndDevelopmentFocusBranch()
-        view = create_initialized_view(product, '+code-index', rootsite='code')
+        view = create_initialized_view(product, '+code-index',
+                                       rootsite='code')
         self.assertIn(branch, view.initial_branches)
 
     def test_initial_branches_does_not_contain_private_dev_focus_branch(self):
         product, branch = self.makeProductAndDevelopmentFocusBranch(
             private=True)
-        view = create_initialized_view(product, '+code-index', rootsite='code')
+        view = create_initialized_view(product, '+code-index',
+                                       rootsite='code')
         self.assertNotIn(branch, view.initial_branches)
 
     def test_committer_count_with_revision_authors(self):
@@ -120,7 +130,8 @@
             date_generator=date_generator)
         getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
 
-        view = create_initialized_view(product, '+code-index', rootsite='code')
+        view = create_initialized_view(product, '+code-index',
+                                       rootsite='code')
         self.assertEqual(view.committer_count, 1)
 
     def test_committers_count_private_branch(self):
@@ -138,10 +149,150 @@
             date_generator=date_generator)
         getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
 
-        view = create_initialized_view(product, '+code-index', rootsite='code')
+        view = create_initialized_view(product, '+code-index',
+                                       rootsite='code')
         self.assertEqual(view.committer_count, 1)
 
 
+class TestProductCodeIndexServiceUsages(ProductTestBase, BrowserTestCase):
+    """Tests for the product code page, especially the usage messasges."""
+
+    def test_external_mirrored(self):
+        # Test that the correct URL is displayed for a mirrored branch.
+        product, branch = self.makeProductAndDevelopmentFocusBranch(
+            branch_type=BranchType.MIRRORED,
+            url="http://example.com/mybranch";)
+        self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        login(ANONYMOUS)
+        content = find_tag_by_id(browser.contents, 'external')
+        text = extract_text(content)
+        expected = ("%(product_title)s hosts its code at %(branch_url)s.  "
+                    "Launchpad has a mirror of the master branch "
+                    "and you can create branches from it." % dict(
+                        product_title=product.title,
+                        branch_url=branch.url))
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+    def test_external_remote(self):
+        # Test that a remote branch is shown properly.
+        product, branch = self.makeProductAndDevelopmentFocusBranch(
+            branch_type=BranchType.REMOTE,
+            url="http://example.com/mybranch";)
+        self.assertEqual(ServiceUsage.EXTERNAL,
+                         product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        login(ANONYMOUS)
+        content = find_tag_by_id(browser.contents, 'external')
+        text = extract_text(content)
+        expected = ("%(product_title)s hosts its code at %(branch_url)s.  "
+                    "Launchpad does not have a copy of the remote "
+                    "branch." % dict(
+                        product_title=product.title,
+                        branch_url=branch.url))
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+    def test_unknown(self):
+        product = self.factory.makeProduct()
+        self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        login(ANONYMOUS)
+        content = find_tag_by_id(browser.contents, 'unknown')
+        text = extract_text(content)
+        expected = (
+            "Launchpad does not know where %(product_title)s "
+            "hosts its code.*" %
+            dict(product_title=product.title))
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+    def test_on_launchpad(self):
+        product, branch = self.makeProductAndDevelopmentFocusBranch()
+        self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        login(ANONYMOUS)
+        text = extract_text(find_tag_by_id(
+            browser.contents, 'branch-count-summary'))
+        expected = "1 active  branch owned by 1 person.*"
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+    def test_view_mirror_location(self):
+        url = "http://example.com/mybranch";
+        product, branch = self.makeProductAndDevelopmentFocusBranch(
+            branch_type=BranchType.MIRRORED,
+            url=url)
+        view = create_initialized_view(product, '+code-index',
+                                       rootsite='code')
+        self.assertEqual(url, view.mirror_location)
+
+
+class TestProductBranchesViewPortlets(ProductTestBase, BrowserTestCase):
+    """Tests for the portlets."""
+
+    def test_portlet_not_shown_for_UNKNOWN(self):
+        # If the BranchUsage is UNKNOWN then the portlets are not shown.
+        product = self.factory.makeProduct()
+        self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        contents = browser.contents
+        self.assertEqual(None, find_tag_by_id(contents, 'branch-portlet'))
+        self.assertEqual(None, find_tag_by_id(contents, 'privacy'))
+        self.assertEqual(None, find_tag_by_id(contents, 'involvement'))
+        self.assertEqual(None, find_tag_by_id(
+            contents, 'portlet-product-codestatistics'))
+
+    def test_portlets_shown_for_HOSTED(self):
+        # If the BranchUsage is HOSTED then the portlets are shown.
+        product, branch = self.makeProductAndDevelopmentFocusBranch()
+        self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        contents = browser.contents
+        self.assertNotEqual(None, find_tag_by_id(contents, 'branch-portlet'))
+        self.assertNotEqual(None, find_tag_by_id(contents, 'privacy'))
+        self.assertNotEqual(None, find_tag_by_id(contents, 'involvement'))
+        self.assertNotEqual(None, find_tag_by_id(
+            contents, 'portlet-product-codestatistics'))
+
+    def test_portlets_shown_for_EXTERNAL(self):
+        # If the BranchUsage is HOSTED then the portlets are shown.
+        url = "http://example.com/mybranch";
+        product, branch = self.makeProductAndDevelopmentFocusBranch(
+            branch_type=BranchType.MIRRORED,
+            url=url)
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        contents = browser.contents
+        self.assertNotEqual(None, find_tag_by_id(contents, 'branch-portlet'))
+        self.assertNotEqual(None, find_tag_by_id(contents, 'privacy'))
+        self.assertNotEqual(None, find_tag_by_id(contents, 'involvement'))
+        self.assertNotEqual(None, find_tag_by_id(
+            contents, 'portlet-product-codestatistics'))
+
+    def test_is_private(self):
+        team_owner = self.factory.makePerson()
+        team = self.factory.makeTeam(team_owner)
+        product = self.factory.makeProduct(owner=team_owner)
+        branch = self.factory.makeProductBranch(product=product)
+        login_person(product.owner)
+        product.development_focus.branch = branch
+        product.setBranchVisibilityTeamPolicy(
+            team, BranchVisibilityRule.PRIVATE)
+        view = create_initialized_view(
+            product, '+code-index', rootsite='code', principal=product.owner)
+        text = extract_text(find_tag_by_id(view.render(), 'privacy'))
+        expected = ("New branches you create for %(name)s are private "
+                    "initially.*" % dict(name=product.displayname))
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+    def test_is_public(self):
+        product = self.factory.makeProduct()
+        branch = self.factory.makeProductBranch(product=product)
+        login_person(product.owner)
+        product.development_focus.branch = branch
+        browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
+        text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
+        expected = ("New branches you create for %(name)s are public "
+                    "initially.*" % dict(name=product.displayname))
+        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)
-

=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt'
--- lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt	2010-01-14 23:39:06 +0000
+++ lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt	2010-09-22 20:23:46 +0000
@@ -63,11 +63,11 @@
 
     >>> browser.open('http://code.launchpad.dev/fooix')
     >>> print_tag_with_id(browser.contents, 'merge-counts')
-    3 active reviews or unmerged proposals
-
-The 'active reviews or unmerged proposals' text links to the active reviews page.
-
-    >>> browser.getLink('active reviews or unmerged proposals').click()
+    3 active reviews
+
+The 'active reviews' text links to the active reviews page.
+
+    >>> browser.getLink('active reviews').click()
     >>> print browser.title
     Active code reviews for Fooix...
 
@@ -94,7 +94,7 @@
     >>> browser.open('http://code.launchpad.dev/~albert')
     >>> print_tag_with_id(browser.contents, 'page-summary')
     1 owned branch, 1 registered branch, 1 subscribed branch
-    1 active review or unmerged proposal
+    1 active review
 
 The person's active reviews also lists all of their currently requested
 reviews.

=== modified file 'lib/lp/code/stories/branches/xx-person-branches.txt'
--- lib/lp/code/stories/branches/xx-person-branches.txt	2010-05-27 02:19:27 +0000
+++ lib/lp/code/stories/branches/xx-person-branches.txt	2010-09-22 20:23:46 +0000
@@ -101,7 +101,7 @@
     >>> eric_browser.open('http://code.launchpad.dev/~eric')
     >>> print_tag_with_id(eric_browser.contents, 'page-summary')
     1 owned branch, 1 registered branch, 1 subscribed branch
-    0 active reviews or unmerged proposals
+    0 active reviews
 
 Now we'll create another branch, and unsubscribe the owner from it.
 
@@ -113,4 +113,4 @@
     >>> eric_browser.open('http://code.launchpad.dev/~eric')
     >>> print_tag_with_id(eric_browser.contents, 'page-summary')
     2 owned branches, 2 registered branches, 1 subscribed branch
-    0 active reviews or unmerged proposals
+    0 active reviews

=== modified file 'lib/lp/code/stories/branches/xx-private-branch-listings.txt'
--- lib/lp/code/stories/branches/xx-private-branch-listings.txt	2010-09-03 00:25:07 +0000
+++ lib/lp/code/stories/branches/xx-private-branch-listings.txt	2010-09-22 20:23:46 +0000
@@ -1,4 +1,5 @@
-= Private Branch Listings =
+Private Branch Listings
+=======================
 
 All pages that show branch listings to users should only show branches
 that the user is allowed to see.
@@ -15,7 +16,8 @@
     ...     reset_all_branch_last_modified)
     >>> reset_all_branch_last_modified()
 
-== Additional sample data ==
+Additional sample data
+----------------------
 
 Adding a private branch that is only visible by No Privileges Person
 (and Launchpad administrators).
@@ -37,7 +39,8 @@
     >>> logout()
 
 
-== The code home page ==
+The code home page
+------------------
 
 The code home page shows lists of recently imported, changed, and
 registered branches.
@@ -60,7 +63,8 @@
 Logged in users should only be able to see public branches, and private
 branches that they are subscribed to or are the owner of.
 
-    >>> no_priv_browser = setupBrowser(auth='Basic no-priv@xxxxxxxxxxxxx:test')
+    >>> no_priv_browser = setupBrowser(
+    ...     auth='Basic no-priv@xxxxxxxxxxxxx:test')
     >>> print_recently_registered_branches(no_priv_browser)
     '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'
     '...~mark/+junk/testdoc...'
@@ -91,7 +95,8 @@
     '...~name12/gnome-terminal/scanned...'
 
 
-== Landscape code listing page ==
+Landscape code listing page
+---------------------------
 
 One of the most obvious places to hide private branches are the code
 listing tab.
@@ -103,12 +108,13 @@
     ...     # So print the text shown in the application summary.
     ...     if table is None:
     ...         print extract_text(find_tag_by_id(
-    ...             browser.contents, 'application-summary'))
+    ...             browser.contents, 'branch-summary'))
     ...     else:
     ...         for row in table.tbody.fetch('tr'):
     ...             print extract_text(row)
 
     >>> print_landscape_code_listing(anon_browser)
+    Launchpad does not know where The Landscape Project hosts its code...
     There are no branches for The Landscape Project in Launchpad...
 
     >>> print_landscape_code_listing(no_priv_browser)
@@ -124,7 +130,8 @@
     lp://dev/~no-priv/landscape/testing-branch      Development   ...
 
 
-== Person code listing pages ==
+Person code listing pages
+-------------------------
 
 The person code listings is the other obvious place to filter out the
 viewable branches.
@@ -173,7 +180,8 @@
     >>> print_person_code_listing(landscape_dev_browser, '/+ownedbranches')
     Total of 10 branches listed
     lp://dev/~name12/landscape/feature-x            Development   ...
-    >>> print_person_code_listing(landscape_dev_browser, '/+registeredbranches')
+    >>> print_person_code_listing(landscape_dev_browser,
+    ...                           '/+registeredbranches')
     Total of 11 branches listed
     lp://dev/~landscape-developers/landscape/trunk  Development   ...
     lp://dev/~name12/landscape/feature-x            Development   ...
@@ -190,7 +198,8 @@
     lp://dev/~name12/landscape/feature-x            Development   ...
 
 
-== Bug branch links ==
+Bug branch links
+----------------
 
 When a private branch is linked to a bug, the bug branch link is only
 visible to those that would be able to see the branch.
@@ -227,7 +236,8 @@
     No bug branch links
 
 
-== Branches set as primary branches for product series ==
+Branches set as primary branches for product series
+---------------------------------------------------
 
 When a branch is set as the user branch for product series, the details
 must be visible to those that are entitled to see it, but hidden from

=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
--- lib/lp/code/stories/branches/xx-product-branches.txt	2010-08-19 14:22:01 +0000
+++ lib/lp/code/stories/branches/xx-product-branches.txt	2010-09-22 20:23:46 +0000
@@ -34,9 +34,10 @@
 If there are not any branches, a helpful message is shown.
 
     >>> def get_summary(browser):
-    ...     return find_tag_by_id(browser.contents, 'application-summary')
+    ...     return find_tag_by_id(browser.contents, 'branch-summary')
     >>> summary = get_summary(browser)
     >>> print extract_text(summary)
+    Launchpad does not know where The Gnome Panel Applets hosts its code.
     There are no branches for Gnome Applets in Launchpad.
     If there are Bazaar branches of Gnome Applets in a publicly
       accessible location, Launchpad can act as a mirror of the branch
@@ -44,7 +45,7 @@
     Launchpad can also act as a primary location for Bazaar branches of
       Gnome Applets. Read more.
     Launchpad can import code from CVS, Subversion, Mercurial or Git
-      into Bazaar branches. Read more.
+      into Bazaar branches. Read more...
 
 The 'Read more' links go to the help wiki.
 
@@ -53,6 +54,7 @@
     https://help.launchpad.net/Code/MirroredBranches
     https://help.launchpad.net/Code/UploadingABranch
     https://help.launchpad.net/VcsImports
+    https://help.launchpad.net/Code
 
 
 Link to the product downloads
@@ -63,6 +65,7 @@
 
     >>> browser.open('http://code.launchpad.dev/netapplet')
     >>> print extract_text(get_summary(browser))
+    Launchpad does not know where Network Applet hosts its code...
     There are no branches for NetApplet in Launchpad.
     ...
     There are download files available for NetApplet.
@@ -83,8 +86,6 @@
     >>> browser.open('http://code.launchpad.dev/evolution')
     >>> summary = get_summary(browser)
     >>> print extract_text(get_summary(browser))
-    3 active branches ...
-    0 active reviews or unmerged proposals
     You can get a copy of the development focus branch using the
     command:
        bzr branch lp://dev/evolution
@@ -124,7 +125,8 @@
 Firstly lets associate release--0.9.1 with the 1.0 series.
 
     >>> admin_browser.open('http://launchpad.dev/firefox/1.0/+linkbranch')
-    >>> admin_browser.getControl('Branch').value = '~mark/firefox/release--0.9.1'
+    >>> admin_browser.getControl('Branch').value = (
+    ...     '~mark/firefox/release--0.9.1')
     >>> admin_browser.getControl('Update').click()
 
     >>> browser.open('http://code.launchpad.dev/firefox')
@@ -155,32 +157,54 @@
     lp://dev/~mark/firefox/release-0.8     Development ...
 
 
-Floating buttons
-================
+Involvement portlet
+===================
 
-There are two buttons that show on the right hand side of the screen
-for project branch listings.  'Register a branch' and 'Import a branch'.
+There are several links in the side portlet:  'Register a branch',
+'Import a branch', 'Configure code hosting', and 'Define branch
+visibility'.  The links are only shown if the user has permission to
+perform the task.
 
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> login('admin@xxxxxxxxxxxxx')
+    >>> from lp.testing.sampledata import ADMIN_EMAIL
+    >>> login(ADMIN_EMAIL)
     >>> product = getUtility(IProductSet).getByName('firefox')
     >>> old_branch = product.development_focus.branch
     >>> product.development_focus.branch = None
     >>> logout()
     >>> def print_links(browser):
-    ...     links = find_tag_by_id(browser.contents, 'floating-links')
+    ...     links = find_tag_by_id(browser.contents, 'involvement')
     ...     for link in links.findAll('a'):
     ...         print extract_text(link)
-    >>> browser.open('http://code.launchpad.dev/firefox')
-    >>> print_links(browser)
+    >>> admin_browser.open('http://code.launchpad.dev/firefox')
+    >>> print_links(admin_browser)
+    Register a branch
+    Import a branch
+    Configure code hosting
+    Define branch visibility
+
+The owner of the project sees the links for the activities he can
+perform, everything except defining branch visibility.
+
+    >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
+    >>> owner_browser.open('http://code.launchpad.dev/firefox')
+    >>> print_links(owner_browser)
+    Register a branch
+    Import a branch
+    Configure code hosting
+
+And a regular user can only register and import branches.
+
+    >>> user_browser.open('http://code.launchpad.dev/firefox')
+    >>> print_links(user_browser)
     Register a branch
     Import a branch
 
 If the product specifies that it officially uses Launchpad code, then
 the 'Import a branch' button is still shown.
 
-    >>> login('admin@xxxxxxxxxxxxx')
+    >>> login(ADMIN_EMAIL)
     >>> product.development_focus.branch = old_branch
     >>> logout()
     >>> browser.open('http://code.launchpad.dev/firefox')
@@ -189,34 +213,45 @@
     Import a branch
 
 
-Nice wording of summary numbers
-===============================
+The statistics portlet
+======================
 
 The text that is shown giving a summary of the number of branches
 shows correct singular and plural forms.
 
-    >>> def print_summary(product):
+    >>> def get_stats_portlet(browser):
+    ...     return find_tag_by_id(
+    ...         browser.contents,
+    ...         'portlet-product-codestatistics')
+    >>> def print_portlet(product):
     ...     browser.open('http://code.launchpad.dev/%s' % product)
-    ...     print extract_text(get_summary(browser))
-
-    >>> print_summary('gnome-terminal')
-    8 active branches owned by 1 person and 2 teams, 0 commits in the last month
-    ...
+    ...     print extract_text(get_stats_portlet(browser))
+
+    >>> print_portlet('gnome-terminal')
+    0 active reviews
+    8 active branches owned by 1 person and 2 teams
+    0 commits in the last month
+
     >>> from lp.testing import ANONYMOUS, login, logout
     >>> login(ANONYMOUS)
     >>> fooix = factory.makeProduct('fooix')
     >>> ignored = factory.makeProductBranch(fooix)
     >>> ignored = factory.makeProductBranch(fooix)
     >>> logout()
-    >>> print_summary('fooix')
-    2 active branches owned by 2 people, 0 commits in the last month
-    ...
-    >>> print_summary('evolution')
-    3 active branches owned by 1 person and 1 team, 0 commits in the last month
-    ...
-    >>> print_summary('iso-codes')
-    1 active branch owned by 1 person, 0 commits in the last month
-    ...
+    >>> print_portlet('fooix')
+    0 active reviews
+    2 active branches owned by 2 people
+    0 commits in the last month
+
+    >>> print_portlet('evolution')
+    0 active reviews
+    3 active branches owned by 1 person and 1 team
+    0 commits in the last month
+
+    >>> print_portlet('iso-codes')
+    0 active reviews
+    1 active branch owned by 1 person
+    0 commits in the last month
 
 
 Product has Branches, but none initially visible
@@ -231,10 +266,10 @@
     >>> admin_browser.getControl('Abandoned').click()
     >>> admin_browser.getControl('Change Branch').click()
 
-    >>> browser.open('http://code.launchpad.dev/iso-codes')
-    >>> print extract_text(get_summary(browser))
-    0 active branches, 0 commits in the last month
-    0 active reviews or unmerged proposals
+    >>> print_portlet('iso-codes')
+    0 active reviews
+    0 active branches
+    0 commits in the last month
 
     >>> message = find_tag_by_id(browser.contents, 'no-branch-message')
     >>> print extract_text(message)

=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
--- lib/lp/code/templates/product-branch-summary.pt	2010-08-13 16:09:45 +0000
+++ lib/lp/code/templates/product-branch-summary.pt	2010-09-22 20:23:46 +0000
@@ -7,7 +7,37 @@
   lang="en"
   dir="ltr"
   i18n:domain="launchpad"
-  id="application-summary">
+  id="branch-summary">
+
+  <div id="unknown" tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
+    <p>
+      <strong>
+        Launchpad does not know where <tal:project_title replace="context/title" />
+        hosts its code.
+      </strong>
+    </p>
+  </div>
+
+  <div id="external"
+       tal:condition="context/codehosting_usage/enumvalue:EXTERNAL">
+    <p>
+      <strong>
+        <tal:project_title replace="context/title" /> hosts its code at
+        <a tal:attributes="href view/mirror_location"><tal:mirror replace="view/mirror_location"/></a>.
+      </strong>
+    </p>
+    <p tal:condition="context/homepageurl">
+      You can learn more at the project's
+      <a tal:attributes="href context/homepageurl">web page</a>.
+    </p>
+    <p tal:condition="view/branch/branch_type/enumvalue:MIRRORED">
+      Launchpad has a mirror of the master branch and you can create branches
+      from it.
+    </p>
+    <p tal:condition="view/branch/branch_type/enumvalue:REMOTE">
+      Launchpad does not have a copy of the remote branch.
+    </p>
+  </div>
 
   <tal:no-branches condition="not: view/branch_count">
     There are no branches for <tal:project-name replace="context/displayname"/>
@@ -34,15 +64,6 @@
   </tal:no-branches>
 
   <tal:has-branches condition="view/branch_count">
-    <p tal:replace="structure context/@@+count-summary"/>
-    <p id="merge-counts"
-       tal:define="menu context/menu:branches">
-      <strong class="count" tal:content="menu/active_review_count">5</strong>
-      <tal:link
-          define="link menu/active_reviews"
-          replace="structure link/render"
-          />
-    </p>
     <div tal:condition="view/has_development_focus_branch"
          style="margin: 1em 0"
          tal:define="config modules/canonical.config/config;
@@ -60,6 +81,30 @@
     </div>
 
   </tal:has-branches>
+
+  <div tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
+    <div
+       tal:condition="not: context/codehosting_usage/enumvalue:LAUNCHPAD"
+       tal:define="configure_codehosting view/configure_codehosting |
+       nothing">
+      <p style="margin-top: 10px;">
+        <a class="sprite maybe"
+           href="https://help.launchpad.net/Code";>Getting started
+          with code hosting in Launchpad</a>.</p>
+
+      <p tal:condition="context/required:launchpad.Edit"
+         id="no-code-edit"
+         >
+        <a tal:condition="configure_codehosting"
+           tal:replace="structure configure_codehosting/fmt:link"/>
+      </p>
+      <p tal:define="menu context/menu:branches;
+                     link menu/branch_visibility"
+         tal:condition="link/enabled"
+         tal:content="structure link/render"></p>
+    </div>
+  </div>
+
   <p tal:condition="view/latest_release_with_download_files">
     <img src="/@@/download"/> There are
     <a tal:define="rooturl modules/canonical.launchpad.webapp.vhosts/allvhosts/configs/mainsite/rooturl"

=== modified file 'lib/lp/code/templates/product-branches.pt'
--- lib/lp/code/templates/product-branches.pt	2009-09-17 00:27:40 +0000
+++ lib/lp/code/templates/product-branches.pt	2010-09-22 20:23:46 +0000
@@ -3,40 +3,66 @@
   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"
+  metal:use-macro="view/macro:page/main_side"
   i18n:domain="launchpad"
 >
 
 <body>
 
-<div metal:fill-slot="main">
-
-  <div style="float:right" id="floating-links"
-       tal:define="menu context/menu:branches">
-    <div tal:define="link menu/branch_add"
-         tal:condition="link/enabled"
-         tal:content="structure link/render" />
-    <div tal:define="link menu/code_import"
-         tal:condition="link/enabled"
-         tal:content="structure link/render" />
-    <div tal:define="link menu/branch_visibility"
-         tal:condition="link/enabled"
-         tal:content="structure link/render" />
-  </div>
-
-  <div id="private-policy" tal:condition="view/new_branches_are_private"
-       class="informational message">
-    New branches you create for <tal:name replace="context/displayname"/>
-    are <strong>private</strong> initially.
-  </div>
-
-  <tal:branch-summary content="structure context/@@+branch-summary" />
-
-  <tal:has-branches condition="view/branch_count"
-                    define="branches view/branches">
-    <tal:branchlisting content="structure branches/@@+branch-listing" />
-  </tal:has-branches>
-</div>
+  <metal:side fill-slot="side" tal:define="context_menu context/menu:context">
+    <div id="branch-portlet"
+         tal:condition="not: context/codehosting_usage/enumvalue:UNKNOWN">
+      <div id="privacy"
+           tal:define="are_private view/new_branches_are_private"
+           tal:attributes="class python: are_private and 'first portlet private' or 'first portlet public'">
+        <div tal:condition="not:view/new_branches_are_private" id="privacy-text">
+          <p>
+            New branches you create for <tal:name replace="context/displayname"/>
+            are <strong>public</strong> initially.
+          </p>
+        </div>
+        <div tal:condition="view/new_branches_are_private" id="privacy-text">
+          <p>
+            New branches you create for <tal:name replace="context/displayname"/>
+            are <strong>private</strong> initially.
+          </p>
+        </div>
+      </div>
+
+      <div id="involvement" class="portlet"
+           tal:define="menu context/menu:branches">
+        <ul class="involvement">
+            <li style="border: none">
+              <a href="+addbranch" class="menu-link-addbranch sprite code">
+                Register a branch
+              </a>
+            </li>
+        </ul>
+        <p tal:define="link menu/code_import"
+           tal:condition="link/enabled"
+           tal:content="structure link/render"></p>
+        <p tal:define="configure_codehosting view/configure_codehosting | nothing"
+           tal:condition="configure_codehosting"
+           tal:replace="structure configure_codehosting/fmt:link"></p>
+        <p tal:define="link menu/branch_visibility"
+           tal:condition="link/enabled"
+           tal:content="structure link/render"></p>
+      </div>
+
+      <div tal:replace="structure context/@@+portlet-product-codestatistics" />
+    </div>
+  </metal:side>
+
+  <tal:main metal:fill-slot="main">
+
+    <tal:branch-summary content="structure context/@@+branch-summary" />
+
+    <tal:has-branches condition="view/branch_count"
+                      define="branches view/branches">
+      <tal:branchlisting content="structure branches/@@+branch-listing" />
+    </tal:has-branches>
+
+  </tal:main>
 
 </body>
 </html>

=== added file 'lib/lp/code/templates/product-portlet-codestatistics-content.pt'
--- lib/lp/code/templates/product-portlet-codestatistics-content.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/product-portlet-codestatistics-content.pt	2010-09-22 20:23:46 +0000
@@ -0,0 +1,60 @@
+<tal:portlet-product-codestatistics-content
+    xmlns:tal="http://xml.zope.org/namespaces/tal";
+    xmlns:metal="http://xml.zope.org/namespaces/metal";>
+  <tal:comment condition="nothing">
+    The view/*_count|nothing expressions below are so that this
+    template can be rendered by a view that does not have count
+    information available.
+  </tal:comment>
+
+  <tr tal:define="menu context/menu:branches" class="code-links"
+      id="merge-counts">
+    <td class="code-count"
+        tal:define="count menu/active_review_count"
+        tal:content="count" />
+    <td>
+      <tal:link
+         define="link menu/active_reviews"
+         replace="structure link/render"
+         />
+    </td>
+  </tr>
+
+  <tr class="code-links" id="branch-count-summary">
+    <td class="code-count"
+        tal:define="count view/branch_count"
+        tal:content="count" />
+    <td>
+  <tal:branches replace="view/branch_text">branches</tal:branches
+  ><tal:has-branches condition="view/branch_count">
+  owned by
+  <tal:individuals condition="view/person_owner_count">
+    <tal:owners content="view/person_owner_count">42</tal:owners>
+    <tal:people replace="view/person_text">people</tal:people
+    ></tal:individuals
+    ><tal:teams condition="view/team_owner_count">
+    <tal:individuals condition="view/person_owner_count">
+      and
+    </tal:individuals>
+    <tal:toc content="view/team_owner_count">1</tal:toc>
+    <tal:people replace="view/team_text">team</tal:people
+    ></tal:teams></tal:has-branches>
+    </td>
+  </tr>
+
+  <tr class="code-links">
+    <td class="code-count"
+        tal:define="count view/commit_count"
+        tal:content="count" />
+    <td>
+    <tal:commits replace="view/commit_text">commits</tal:commits>
+    <tal:has-committers condition="view/committer_count">
+      by
+      <tal:cc content="view/committer_count">4</tal:cc>
+      <tal:people replace="view/committer_text">people</tal:people>
+    </tal:has-committers>
+    in the last month
+    </td>
+  </tr>
+
+</tal:portlet-product-codestatistics-content>

=== added file 'lib/lp/code/templates/product-portlet-codestatistics.pt'
--- lib/lp/code/templates/product-portlet-codestatistics.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/product-portlet-codestatistics.pt	2010-09-22 20:23:46 +0000
@@ -0,0 +1,11 @@
+<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-product-codestatistics">
+
+  <table class="code-links">
+    <tbody id="portlet-product-codestatistics"
+      tal:content="structure context/@@+portlet-product-codestatistics-content" />
+  </table>
+</div>

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2010-09-13 12:09:30 +0000
+++ lib/lp/registry/browser/pillar.py	2010-09-22 20:23:46 +0000
@@ -79,7 +79,7 @@
 
     def submit_code(self):
         if self.pillar.codehosting_usage in [
-                ServiceUsage.LAUNCHPAD, 
+                ServiceUsage.LAUNCHPAD,
                 ServiceUsage.EXTERNAL,
                 ]:
             enabled = True

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2010-09-15 16:03:14 +0000
+++ lib/lp/registry/browser/product.py	2010-09-22 20:23:46 +0000
@@ -468,8 +468,6 @@
         # Add the branch configuration in separately.
         set_branch = series_menu['set_branch']
         set_branch.text = 'Configure project branch'
-        set_branch.configured = (
-            )
         config_list.append(
             dict(link=set_branch,
                  configured=config_statuses['configure_codehosting']))
@@ -478,11 +476,8 @@
     @property
     def registration_completeness(self):
         """The percent complete for registration."""
-        configured = 0
         config_statuses = self.configuration_states
-        for key, value in config_statuses.items():
-            if value:
-                configured += 1
+        configured = sum([1 for v in config_statuses.values() if v])
         scale = 100
         done = int(float(configured) / len(config_statuses) * scale)
         undone = scale - done
@@ -1405,9 +1400,9 @@
     def setUpFields(self):
         super(ProductConfigureBase, self).setUpFields()
         if self.usage_fieldname is not None:
-            # The usage fields are shared among pillars.  But when referring to
-            # an individual object in Launchpad it is better to call it by its
-            # real name, i.e. 'project' instead of 'pillar'.
+            # The usage fields are shared among pillars.  But when referring
+            # to an individual object in Launchpad it is better to call it by
+            # its real name, i.e. 'project' instead of 'pillar'.
             usage_field = self.form_fields.get(self.usage_fieldname)
             if usage_field:
                 usage_field.custom_widget = CustomWidgetFactory(

=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt	2010-09-16 13:30:18 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt	2010-09-22 20:23:46 +0000
@@ -182,6 +182,17 @@
     >>> print view.codehosting_usage.name
     LAUNCHPAD
 
+    >>> from lp.code.enums import BranchType
+    >>> remote = factory.makeProduct()
+    >>> branch = factory.makeProductBranch(product=remote,
+    ...                                    branch_type=BranchType.REMOTE)
+    >>> remote.official_codehosting
+    False
+    >>> view = create_view(remote, '+get-involved')
+    >>> print view.codehosting_usage.name
+    UNKNOWN
+
+
 Project groups cannot make links to register a branch, so
 official_codehosting is always false.
 

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2010-09-14 15:04:06 +0000
+++ lib/lp/registry/model/product.py	2010-09-22 20:23:46 +0000
@@ -49,9 +49,6 @@
     SQLBase,
     sqlvalues,
     )
-from canonical.launchpad.components.decoratedresultset import (
-    DecoratedResultSet,
-    )
 from canonical.launchpad.interfaces.launchpad import (
     IHasIcon,
     IHasLogo,
@@ -404,7 +401,8 @@
             return ServiceUsage.UNKNOWN
         elif self.development_focus.branch.branch_type == BranchType.HOSTED:
             return ServiceUsage.LAUNCHPAD
-        elif self.development_focus.branch.branch_type == BranchType.MIRRORED:
+        elif self.development_focus.branch.branch_type in (
+            BranchType.MIRRORED, BranchType.REMOTE):
             return ServiceUsage.EXTERNAL
         return ServiceUsage.NOT_APPLICABLE
 

=== modified file 'lib/lp/registry/tests/test_service_usage.py'
--- lib/lp/registry/tests/test_service_usage.py	2010-09-21 14:47:26 +0000
+++ lib/lp/registry/tests/test_service_usage.py	2010-09-22 20:23:46 +0000
@@ -8,6 +8,7 @@
 from canonical.testing import DatabaseFunctionalLayer
 
 from lp.app.enums import ServiceUsage
+from lp.code.enums import BranchType
 from lp.testing import (
     login_person,
     TestCaseWithFactory,
@@ -56,13 +57,6 @@
             True,
             self.target.official_answers)
 
-    def test_codehosting_usage(self):
-        # Only test get for codehosting; this has no setter because the
-        # state is derived from other data.
-        self.assertEqual(
-            ServiceUsage.UNKNOWN,
-            self.target.codehosting_usage)
-
     def test_translations_usage_no_data(self):
         # By default, we don't know anything about a target
         self.assertEqual(
@@ -194,6 +188,42 @@
         super(TestProductUsageEnums, self).setUp()
         self.target = self.factory.makeProduct()
 
+    def test_codehosting_unknown(self):
+        # A default product has UNKNOWN usage.
+        self.assertEqual(
+            ServiceUsage.UNKNOWN,
+            self.target.codehosting_usage)
+
+    def test_codehosting_mirrored_branch(self):
+        # A mirrored branch is EXTERNAL.
+        login_person(self.target.owner)
+        self.target.development_focus.branch = self.factory.makeProductBranch(
+            product=self.target,
+            branch_type=BranchType.MIRRORED)
+        self.assertEqual(
+            ServiceUsage.EXTERNAL,
+            self.target.codehosting_usage)
+
+    def test_codehosting_remote_branch(self):
+        # A remote branch is EXTERNAL.
+        login_person(self.target.owner)
+        self.target.development_focus.branch = self.factory.makeProductBranch(
+            product=self.target,
+            branch_type=BranchType.REMOTE)
+        self.assertEqual(
+            ServiceUsage.EXTERNAL,
+            self.target.codehosting_usage)
+
+    def test_codehosting_hosted_branch(self):
+        # A branch on Launchpad is HOSTED.
+        login_person(self.target.owner)
+        self.target.development_focus.branch = self.factory.makeProductBranch(
+            product=self.target,
+            branch_type=BranchType.HOSTED)
+        self.assertEqual(
+            ServiceUsage.LAUNCHPAD,
+            self.target.codehosting_usage)
+
 
 class TestProductSeriesUsageEnums(
     TestCaseWithFactory,


Follow ups