← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/official-services-0 into lp:launchpad/devel

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/official-services-0 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


This is my first branch to state the location of a project's official services
on the its app pages. This branch adds the unknown service presentation to
Answers

    lp:~sinzui/launchpad/official-services-0
    Diff size: 639
    Launchpad bug:
          https://bugs.launchpad.net/bugs/597738
    Test command: ./bin/test -vv \
          -t test_questiontarget -t TestDistributionSourcePackageFormatterAPI
          -t faq-browse -t question-browse -t question-search-multiple
    Pre-implementation: jml, bac
    Target release: 10.09


State that Launchpad does not know where Support is hosted
----------------------------------------------------------

This branch deals with the unknown condition for Launchpad Answers. A simple
page explaining the condition is shown. There is a link the Ubuntu DSP if
the project is linked to an Ubuntu package. Project owners can see a link
to configure Answers.

I decided not to work on the remote condition because it requires a schema
change to record the remote forum or mailing list. We will consider
addressing this issue in another branch.


Rules
-----

    * When a service is unknown, the root page must say so and the feature
      can be enabled. The page must explain what the app provides and how
      it can be enabled. It may need to explain that the feature can be
      enabled without being official
      * Enabled can be explicit in the case of answers
      * Answers needs an explanation of the service.
    * Questions defines the template in python. This makes changing the
      template based on the object state easy, but the Person views need to
      be decoupled or redefined since it is also a questioncollection.
    * NB. I added DistributionSourcePackageFormatterAPI so that there is an
      easy way to create a link to a DSP.

QA
--

    * Visit https://answers.edge.launchpad.net/dtdparser
    * Verify that the page explains that Launchpad does not know where
      support requests are handled.
    * Follow the configure support link and set support to Launchpad.
    * Verify that the answers page is now enabled.
    * Visit https://answers.edge.launchpad.net/gedit
    * Verify that the page explains that Launchpad does not know where
      support requests are handled.
    * Verify there is a link to ask a question at
      https://answers.edge.launchpad.net/ubuntu/+source/gedit


Lint
----

Linting changed files:
  lib/lp/answers/browser/questiontarget.py
  lib/lp/answers/browser/tests/test_questiontarget.py
  lib/lp/answers/stories/faq-browse-and-search.txt
  lib/lp/answers/stories/question-browse-and-search.txt
  lib/lp/answers/stories/question-search-multiple-languages.txt
  lib/lp/answers/templates/unknown-support.pt
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/distribution.py
  lib/lp/registry/browser/distributionsourcepackage.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/browser/tests/test_distributionsourcepackage.py

NB. lint reports lots line length and header issues in the doctests. I
    can fix these before I land the branch.


Test
----

    * lib/lp/answers/browser/tests/test_questiontarget.py
      * Added tests for template conditions
      * Added tests for DSP that the page can link to.
      * Added tests to verify when the configuration link can appear.
    * lib/lp/answers/stories/faq-browse-and-search.txt
      * Enabled Launchpad Answers to keep the test working.
    * lib/lp/answers/stories/question-browse-and-search.txt
      * Enabled Launchpad Answers to keep the test working.
    * lib/lp/answers/stories/question-search-multiple-languages.txt
      * Enabled Launchpad Answers to keep the test working.
    * lib/lp/registry/browser/tests/test_distributionsourcepackage.py
      * Added a test to verify the DSP link formatter.


Implementation
--------------

    * lib/lp/answers/browser/questiontarget.py
      * Added a selected_template property that allows the view to select
        the template based on whether the project officially use answers.
      * Added properties to get the packages linked to this project that
        can provide links to ask questions in Ubuntu.
      * Added a helper for the template to know when to show the link to
        configure support.
    * lib/lp/answers/templates/unknown-support.pt
      * Added a template for the unknown support page.
    * lib/lp/registry/browser/configure.zcml
      * Registered the link formatter for DSPs
    * lib/lp/registry/browser/distribution.py
      * Added a link to configure_answers.
    * lib/lp/registry/browser/distributionsourcepackage.py
      * Added a DistributionSourcePackageFormatterAPI to format DSP links.
    * lib/lp/registry/browser/person.py
      * Updated PersonSearchQuestionsView to define only one template for it
        to use.
      * Updated all other person-questioncollection views to use the
        updated template as a base. This also allowed me to remove a redundant
        class attr.
-- 
https://code.launchpad.net/~sinzui/launchpad/official-services-0/+merge/33675
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/official-services-0 into lp:launchpad/devel.
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py	2010-08-20 20:31:18 +0000
+++ lib/lp/answers/browser/questiontarget.py	2010-08-25 17:41:21 +0000
@@ -29,6 +29,7 @@
 from zope.app.form.browser import DropdownWidget
 from zope.component import (
     getUtility,
+    getMultiAdapter,
     queryMultiAdapter,
     )
 from zope.formlib import form
@@ -61,10 +62,12 @@
     stepto,
     urlappend,
     )
+from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.batching import BatchNavigator
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.launchpad.webapp.menu import structured
 from canonical.widgets import LabeledMultiCheckBoxWidget
+from lp.app.errors import NotFoundError
 from lp.answers.browser.faqcollection import FAQCollectionMenu
 from lp.answers.interfaces.faqcollection import IFAQCollection
 from lp.answers.interfaces.questioncollection import (
@@ -77,8 +80,8 @@
     IQuestionTarget,
     ISearchQuestionsForm,
     )
-from lp.app.errors import NotFoundError
 from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.projectgroup import IProjectGroup
 from lp.services.fields import PublicPersonChoice
 from lp.services.worlddata.interfaces.language import ILanguageSet
@@ -188,6 +191,30 @@
                   orientation='horizontal')
 
     template = ViewPageTemplateFile('../templates/question-listing.pt')
+    unknown_template = ViewPageTemplateFile('../templates/unknown-support.pt')
+
+    @property
+    def selected_template(self):
+        """The template to render the presentation.
+
+        Subclasses can redefine this property to choose their own template.
+        """
+        if IQuestionSet.providedBy(self.context):
+            return self.template
+        involvement = getMultiAdapter(
+            (self.context, self.request), name='+get-involved')
+        if involvement.official_answers:
+            # Primary contexts that officially use answers have a
+            # search and listing presentation.
+            return self.template
+        else:
+            # Primary context that do not officially use answers have an
+            # an explanation about about the current state.
+            return self.unknown_template
+
+    def render(self):
+        """See `LaunchpadView`."""
+        return self.selected_template()
 
     @property
     def page_title(self):
@@ -477,6 +504,31 @@
                 canonical_url(sourcepackage, rootsite='answers'),
                 question.sourcepackagename.name)
 
+    @property
+    def ubuntu_packages(self):
+        """The Ubuntu `IDistributionSourcePackage`s linked to the context.
+
+        If the context is an `IProduct` and it has `IPackaging` links to
+        Ubuntu, a list is returned. Otherwise None is returned
+        """
+        if IProduct.providedBy(self.context):
+            ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+            packages = [
+                package for package in self.context.distrosourcepackages
+                if package.distribution == ubuntu]
+            if len(packages) > 0:
+                return packages
+        return None
+
+    @property
+    def can_configure_answers(self):
+        """Can the user configure answers for the `IQuestionTarget`."""
+        target = self.context
+        if IProduct.providedBy(target) or IDistribution.providedBy(target):
+            return check_permission('launchpad.Edit', self.context)
+        else:
+            return False
+
 
 class QuestionCollectionMyQuestionsView(SearchQuestionsView):
     """SearchQuestionsView specialization for the 'My questions' report.

=== added file 'lib/lp/answers/browser/tests/test_questiontarget.py'
--- lib/lp/answers/browser/tests/test_questiontarget.py	1970-01-01 00:00:00 +0000
+++ lib/lp/answers/browser/tests/test_questiontarget.py	2010-08-25 17:41:21 +0000
@@ -0,0 +1,188 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test questiontarget views."""
+
+__metaclass__ = type
+
+import os
+
+from BeautifulSoup import BeautifulSoup
+
+from zope.component import getUtility
+
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.testing import DatabaseFunctionalLayer
+from lp.answers.interfaces.questioncollection import IQuestionSet
+from lp.app.enums import ServiceUsage
+from lp.testing import login_person, person_logged_in, TestCaseWithFactory
+from lp.testing.views import create_initialized_view
+
+
+class TestSearchQuestionsView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def linkPackage(self, product, name):
+        # A helper to setup a legitimate Packaging link between a product
+        # and an Ubuntu source package.
+        hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
+        sourcepackagename = self.factory.makeSourcePackageName(name)
+        sourcepackage = self.factory.makeSourcePackage(
+            sourcepackagename=sourcepackagename, distroseries=hoary)
+        self.factory.makeSourcePackagePublishingHistory(
+            sourcepackagename=sourcepackagename, distroseries=hoary)
+        product.development_focus.setPackaging(
+            hoary, sourcepackagename, product.owner)
+
+
+class TestSearchQuestionsViewCanConfigureAnswers(TestSearchQuestionsView):
+
+    def test_can_configure_answers_product_no_edit_permission(self):
+        product = self.factory.makeProduct()
+        view = create_initialized_view(product, '+questions')
+        self.assertEqual(False, view.can_configure_answers)
+
+    def test_can_configure_answers_product_with_edit_permission(self):
+        product = self.factory.makeProduct()
+        login_person(product.owner)
+        view = create_initialized_view(product, '+questions')
+        self.assertEqual(True, view.can_configure_answers)
+
+    def test_can_configure_answers_distribution_no_edit_permission(self):
+        distribution = self.factory.makeDistribution()
+        view = create_initialized_view(distribution, '+questions')
+        self.assertEqual(False, view.can_configure_answers)
+
+    def test_can_configure_answers_distribution_with_edit_permission(self):
+        distribution = self.factory.makeDistribution()
+        login_person(distribution.owner)
+        view = create_initialized_view(distribution, '+questions')
+        self.assertEqual(True, view.can_configure_answers)
+
+    def test_can_configure_answers_projectgroup_with_edit_permission(self):
+        project_group = self.factory.makeProject()
+        login_person(project_group.owner)
+        view = create_initialized_view(project_group, '+questions')
+        self.assertEqual(False, view.can_configure_answers)
+
+    def test_can_configure_answers_dsp_with_edit_permission(self):
+        dsp = self.factory.makeDistributionSourcePackage()
+        login_person(dsp.distribution.owner)
+        view = create_initialized_view(dsp, '+questions')
+        self.assertEqual(False, view.can_configure_answers)
+
+
+class TestSearchQuestionsViewSelectedTemplate(TestSearchQuestionsView):
+    """Test the behaviour of SearchQuestionsView.selected_template"""
+
+    def assertViewTemplate(self, context, file_name):
+        view = create_initialized_view(context, '+questions')
+        self.assertEqual(
+            file_name, os.path.basename(view.selected_template.filename))
+
+    def test_template_product_answers_usage_unknown(self):
+        product = self.factory.makeProduct()
+        self.assertViewTemplate(product, 'unknown-support.pt')
+
+    def test_template_product_answers_usage_launchpad(self):
+        product = self.factory.makeProduct()
+        with person_logged_in(product.owner) as owner:
+            product.answers_usage = ServiceUsage.LAUNCHPAD
+        self.assertViewTemplate(product, 'question-listing.pt')
+
+    def test_template_projectgroup_answers_usage_unknown(self):
+        product = self.factory.makeProduct()
+        project_group = self.factory.makeProject(owner=product.owner)
+        with person_logged_in(product.owner) as owner:
+            product.project = project_group
+        self.assertViewTemplate(project_group, 'unknown-support.pt')
+
+    def test_template_projectgroup_answers_usage_launchpad(self):
+        product = self.factory.makeProduct()
+        project_group = self.factory.makeProject(owner=product.owner)
+        with person_logged_in(product.owner) as owner:
+            product.project = project_group
+            product.answers_usage = ServiceUsage.LAUNCHPAD
+        self.assertViewTemplate(project_group, 'question-listing.pt')
+
+    def test_template_distribution_answers_usage_unknown(self):
+        distribution = self.factory.makeDistribution()
+        self.assertViewTemplate(distribution, 'unknown-support.pt')
+
+    def test_template_distribution_answers_usage_launchpad(self):
+        distribution = self.factory.makeDistribution()
+        with person_logged_in(distribution.owner) as owner:
+            distribution.answers_usage = ServiceUsage.LAUNCHPAD
+        self.assertViewTemplate(distribution, 'question-listing.pt')
+
+    def test_template_DSP_answers_usage_unknown(self):
+        dsp = self.factory.makeDistributionSourcePackage()
+        self.assertViewTemplate(dsp, 'unknown-support.pt')
+
+    def test_template_DSP_answers_usage_launchpad(self):
+        dsp = self.factory.makeDistributionSourcePackage()
+        with person_logged_in(dsp.distribution.owner) as owner:
+            dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD
+        self.assertViewTemplate(dsp, 'question-listing.pt')
+
+    def test_template_question_set(self):
+        question_set = getUtility(IQuestionSet)
+        self.assertViewTemplate(question_set, 'question-listing.pt')
+
+
+class TestSearchQuestionsView_ubuntu_packages(TestSearchQuestionsView):
+    """Test the behaviour of SearchQuestionsView.ubuntu_packages."""
+
+    def test_nonproduct_ubuntu_packages(self):
+        distribution = self.factory.makeDistribution()
+        view = create_initialized_view(distribution, '+questions')
+        packages = view.ubuntu_packages
+        self.assertEqual(None, packages)
+
+    def test_product_ubuntu_packages_unlinked(self):
+        product = self.factory.makeProduct()
+        view = create_initialized_view(product, '+questions')
+        packages = view.ubuntu_packages
+        self.assertEqual(None, packages)
+
+    def test_product_ubuntu_packages_linked(self):
+        product = self.factory.makeProduct()
+        self.linkPackage(product, 'cow')
+        view = create_initialized_view(product, '+questions')
+        packages = view.ubuntu_packages
+        self.assertEqual(1, len(packages))
+        self.assertEqual('cow', packages[0].name)
+
+
+class TestSearchQuestionsViewUnknown(TestSearchQuestionsView):
+    """Test the behaviour of SearchQuestionsView unknown support."""
+
+    def setUp(self):
+        super(TestSearchQuestionsViewUnknown, self).setUp()
+        self.product = self.factory.makeProduct()
+        self.view = create_initialized_view(self.product, '+questions')
+
+    def assertCommonPageElements(self, content):
+        robots = content.find('meta', attrs={'name': 'robots'})
+        self.assertEqual('noindex,nofollow', robots['content'])
+        self.assertTrue(content.find(True, id='support-unknown') is not None)
+
+    def test_any_question_target_any_user(self):
+        content = BeautifulSoup(self.view())
+        self.assertCommonPageElements(content)
+
+    def test_product_with_packaging_elements(self):
+        self.linkPackage(self.product, 'cow')
+        content = BeautifulSoup(self.view())
+        self.assertCommonPageElements(content)
+        self.assertTrue(content.find(True, id='ubuntu-support') is not None)
+
+    def test_product_with_edit_permission(self):
+        login_person(self.product.owner)
+        self.view = create_initialized_view(
+            self.product, '+questions', principal=self.product.owner)
+        content = BeautifulSoup(self.view())
+        self.assertCommonPageElements(content)
+        self.assertTrue(
+            content.find(True, id='configure-support') is not None)

=== modified file 'lib/lp/answers/stories/faq-browse-and-search.txt'
--- lib/lp/answers/stories/faq-browse-and-search.txt	2009-10-31 15:04:48 +0000
+++ lib/lp/answers/stories/faq-browse-and-search.txt	2010-08-25 17:41:21 +0000
@@ -10,6 +10,17 @@
 find her problem there (surely playing a DVD must be common thing to do
 with a computer these days).
 
+    >>> # Kubuntu must enable answers to access questions.
+    >>> from zope.component import getUtility
+    >>> from lp.app.enums import ServiceUsage
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+
+    >>> login('admin@xxxxxxxxxxxxx')
+    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
+    ...     ServiceUsage.LAUNCHPAD)
+    >>> transaction.commit()
+    >>> logout()
+
     >>> browser.open('http://answers.launchpad.dev/kubuntu')
     >>> browser.getLink('All FAQs').click()
 

=== modified file 'lib/lp/answers/stories/question-browse-and-search.txt'
--- lib/lp/answers/stories/question-browse-and-search.txt	2010-04-16 15:06:55 +0000
+++ lib/lp/answers/stories/question-browse-and-search.txt	2010-08-25 17:41:21 +0000
@@ -10,6 +10,17 @@
 system and goes to the Kubuntu's support page in Launchpad to see if
 somebody had a similar problem.
 
+    >>> # Kubuntu must enable answers to access questions.
+    >>> from zope.component import getUtility
+    >>> from lp.app.enums import ServiceUsage
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+
+    >>> login('admin@xxxxxxxxxxxxx')
+    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
+    ...     ServiceUsage.LAUNCHPAD)
+    >>> transaction.commit()
+    >>> logout()
+
     >>> browser.open('http://launchpad.dev/kubuntu')
     >>> browser.getLink('Answers').click()
 
@@ -398,6 +409,14 @@
 If the user didn't make any questions on the product, a message
 informing him of this fact is displayed.
 
+    >>> # gnmomebaker must enable answers to access questions.
+    >>> from lp.registry.interfaces.product import IProductSet
+    >>> login('admin@xxxxxxxxxxxxx')
+    >>> getUtility(IProductSet)['gnomebaker'].answers_usage = (
+    ...     ServiceUsage.LAUNCHPAD)
+    >>> transaction.commit()
+    >>> logout()
+
     >>> sample_person_browser.open(
     ...     'http://launchpad.dev/gnomebaker/+questions')
     >>> sample_person_browser.getLink('My questions').click()

=== modified file 'lib/lp/answers/stories/question-search-multiple-languages.txt'
--- lib/lp/answers/stories/question-search-multiple-languages.txt	2009-09-23 14:40:53 +0000
+++ lib/lp/answers/stories/question-search-multiple-languages.txt	2010-08-25 17:41:21 +0000
@@ -86,6 +86,17 @@
 When the project has no questions to search, we do not show the
 language controls.
 
+    >>> # Kubuntu must enable answers to access questions.
+    >>> from zope.component import getUtility
+    >>> from lp.app.enums import ServiceUsage
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+
+    >>> login('admin@xxxxxxxxxxxxx')
+    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
+    ...     ServiceUsage.LAUNCHPAD)
+    >>> transaction.commit()
+    >>> logout()
+
     >>> anon_browser.open('http://launchpad.dev/kubuntu/+questions')
     >>> anon_browser.getControl(name='field.language')
     Traceback (most recent call last):

=== added file 'lib/lp/answers/templates/unknown-support.pt'
--- lib/lp/answers/templates/unknown-support.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/answers/templates/unknown-support.pt	2010-08-25 17:41:21 +0000
@@ -0,0 +1,44 @@
+<html
+  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="launchpad">
+  <head>
+    <tal:head_epilogue metal:fill-slot="head_epilogue">
+      <meta name="robots" content="noindex,nofollow" />
+    </tal:head_epilogue>
+  </head>
+
+  <body>
+    <div metal:fill-slot="main">
+      <div class="top-portlet">
+        <p id="support-unknown">
+          <strong>Launchpad does not know where
+          <tal:project replace="context/displayname" />
+          tracks support requests.</strong>
+        </p>
+
+        <p id="ubuntu-support"
+          tal:define="packages view/ubuntu_packages"
+          tal:condition="packages">
+          <tal:project replace="context/displayname" /> questions are also
+          tracked in: <tal:packages repeat="package packages">
+            <tal:package replace="structure package/fmt:link" /><tal:comma
+            condition="not:repeat/package/end">, </tal:comma></tal:packages>.
+        </p>
+
+        <p id="configure-support"
+          tal:condition="view/can_configure_answers">
+          Launchpad allows your project to track questions and create FAQs.
+          <br /><a class="sprite maybe"
+            href="https://help.launchpad.net/Answers";>Getting started
+          with support tracking in Launchpad</a>.
+          <br /><a tal:replace="structure context/menu:overview/configure_answers/fmt:link" />
+        </p>
+      </div>
+    </div>
+  </body>
+</html>
+

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-08-13 21:30:24 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-08-25 17:41:21 +0000
@@ -470,6 +470,12 @@
             DistributionSourcePackageFacets
             DistributionSourcePackageOverviewMenu"
         module="lp.registry.browser.distributionsourcepackage"/>
+    <adapter
+        for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
+        provides="zope.traversing.interfaces.IPathAdapter"
+        factory="lp.registry.browser.distributionsourcepackage.DistributionSourcePackageFormatterAPI"
+        name="fmt"
+        />
     <browser:url
         for="lp.registry.interfaces.commercialsubscription.ICommercialSubscription"
         path_expression="string:+commercialsubscription/${id}"

=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2010-08-23 09:10:10 +0000
+++ lib/lp/registry/browser/distribution.py	2010-08-25 17:41:21 +0000
@@ -319,7 +319,8 @@
              'builds', 'cdimage_mirrors', 'archive_mirrors',
              'pending_review_mirrors', 'disabled_mirrors',
              'unofficial_mirrors', 'newmirror', 'announce', 'announcements',
-             'ppas',]
+             'ppas', 'configure_answers',
+             ]
 
     @enabled_with_permission('launchpad.Edit')
     def branding(self):
@@ -424,6 +425,12 @@
         text = 'Personal Package Archives'
         return Link('+ppas', text, icon='info')
 
+    @enabled_with_permission('launchpad.Edit')
+    def configure_answers(self):
+        text = 'Configure support tracker'
+        summary = 'Allow users to ask questions on this project'
+        return Link('+edit', text, summary, icon='edit')
+
 
 class DerivativeDistributionOverviewMenu(DistributionOverviewMenu):
 
@@ -582,6 +589,7 @@
 
         return self.has_exact_matches
 
+
 class DistributionView(HasAnnouncementsView, FeedsMixin, UsesLaunchpadMixin):
     """Default Distribution view class."""
 

=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
--- lib/lp/registry/browser/distributionsourcepackage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/distributionsourcepackage.py	2010-08-25 17:41:21 +0000
@@ -44,6 +44,7 @@
     NavigationMenu,
     )
 from canonical.launchpad.webapp.sorting import sorted_dotted_numbers
+from canonical.launchpad.webapp.tales import CustomizableFormatter
 from canonical.lazr.utils import smartquote
 from lp.answers.browser.questiontarget import (
     QuestionTargetFacetMixin,
@@ -71,8 +72,18 @@
     )
 from lp.soyuz.interfaces.packagediff import IPackageDiffSet
 from lp.translations.browser.customlanguagecode import (
-    HasCustomLanguageCodesTraversalMixin,
-    )
+    HasCustomLanguageCodesTraversalMixin)
+
+
+class DistributionSourcePackageFormatterAPI(CustomizableFormatter):
+    """Adapt IDistributionSourcePackage objects to a formatted string."""
+
+    _link_permission = 'zope.Public'
+    _link_summary_template = '%(displayname)s'
+
+    def _link_summary_values(self):
+        displayname = self._context.displayname
+        return {'displayname': displayname}
 
 
 class DistributionSourcePackageBreadcrumb(Breadcrumb):

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2010-08-23 03:25:20 +0000
+++ lib/lp/registry/browser/person.py	2010-08-25 17:41:21 +0000
@@ -4968,13 +4968,16 @@
 
 
 class PersonSearchQuestionsView(SearchQuestionsView):
-    """View used to search and display questions in which an IPerson is
-    involved.
-    """
+    """View to search and display questions that involve an `IPerson`."""
 
     display_target_column = True
 
     @property
+    def selected_template(self):
+        # Persons always show the default template.
+        return self.template
+
+    @property
     def pageheading(self):
         """See `SearchQuestionsView`."""
         return _('Questions involving $name',
@@ -4988,11 +4991,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchAnsweredQuestionsView(SearchQuestionsView):
+class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
     """View used to search and display questions answered by an IPerson."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(participation=QuestionParticipation.ANSWERER)
@@ -5011,11 +5012,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchAssignedQuestionsView(SearchQuestionsView):
+class SearchAssignedQuestionsView(PersonSearchQuestionsView):
     """View used to search and display questions assigned to an IPerson."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(participation=QuestionParticipation.ASSIGNEE)
@@ -5034,11 +5033,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchCommentedQuestionsView(SearchQuestionsView):
+class SearchCommentedQuestionsView(PersonSearchQuestionsView):
     """View used to search and show questions commented on by an IPerson."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(participation=QuestionParticipation.COMMENTER)
@@ -5057,11 +5054,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchCreatedQuestionsView(SearchQuestionsView):
+class SearchCreatedQuestionsView(PersonSearchQuestionsView):
     """View used to search and display questions created by an IPerson."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(participation=QuestionParticipation.OWNER)
@@ -5080,11 +5075,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchNeedAttentionQuestionsView(SearchQuestionsView):
+class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
     """View used to search and show questions needing an IPerson attention."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(needs_attention=True)
@@ -5102,11 +5095,9 @@
                  mapping=dict(name=self.context.displayname))
 
 
-class SearchSubscribedQuestionsView(SearchQuestionsView):
+class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
     """View used to search and show questions subscribed to by an IPerson."""
 
-    display_target_column = True
-
     def getDefaultFilter(self):
         """See `SearchQuestionsView`."""
         return dict(participation=QuestionParticipation.SUBSCRIBER)

=== added file 'lib/lp/registry/browser/tests/test_distributionsourcepackage.py'
--- lib/lp/registry/browser/tests/test_distributionsourcepackage.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_distributionsourcepackage.py	2010-08-25 17:41:21 +0000
@@ -0,0 +1,26 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test distributionsourcepackage views."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.testing import DatabaseFunctionalLayer
+from lp.testing import test_tales, TestCaseWithFactory
+
+
+class TestDistributionSourcePackageFormatterAPI(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_link(self):
+        sourcepackagename = self.factory.makeSourcePackageName('mouse')
+        ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+        dsp = ubuntu.getSourcePackage('mouse')
+        markup = (
+            u'<a href="/ubuntu/+source/mouse" class="sprite package-source">'
+            u'mouse in ubuntu</a>')
+        self.assertEqual(markup, test_tales('dsp/fmt:link', dsp=dsp))