← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/team-titles into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/team-titles into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #244558 in Launchpad itself: "Not obvious that teams can't have their own bug reports"
  https://bugs.launchpad.net/launchpad/+bug/244558
  Bug #516485 in Launchpad itself: "No User Name in the Title of Related Bugs Page Users/team"
  https://bugs.launchpad.net/launchpad/+bug/516485
  Bug #533044 in Launchpad itself: "Resummarizing bug report doesn't change page title"
  https://bugs.launchpad.net/launchpad/+bug/533044
  Bug #928234 in Launchpad itself: "Team application pages aren't obviously about a team"
  https://bugs.launchpad.net/launchpad/+bug/928234

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/team-titles/+merge/93675

Use standard page titles, bread crumbs, and headings for teams and bugs.

    Pre-implementation: No one. I largely followed the advise of mpt in
    the bug reports.

Bugs Bug #244558 Not obvious that teams can't have their own bug reports
    Page does not clearly state you are looking at a team. The
    page title, heading, and bread crumbs do not conform to Lp rules.
    Many users mistake the team for a project :(

Bug #516485 No User Name in the Title of Related Bugs Page Users/team
    The bug vost brecrumb adapter is not registered for IPerson. The
    adaption fails during traversal every time. When a view is careful
    to provide page_title, a few crumbs appear.

Bug #928234 Team application pages aren't obviously about a team
    Application page headings do not state you are looking at a team.
    mpt suggests that we smartquote the team and append 'team' as is
    done in the bread crumbs.

Bug #533044 Resummarizing bug report doesn't change page title
    Changing the bug title does not update the page title...
    but the bug title should not be in the page title.

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

RULES

    * All four bugs are are caused by developer confusion about how
      Lp page titles, bread crumbs and headings work. In some
      cases the views intentionally deviate from Lp rules.
    * Remove support for override_title_breadcrumbs
      * Instead check for an instances SystemErrorView.
      * Removing override_title_breadcrumbs will restore the page title
        and breadcrumbs to several bug pages and specifically team bug
        pages.
      * This partially addresses the concern that bug titles can leak
        confidential information when the user has limited view.
    * Every traversed object must have a breadcrumb adapter.
      * Register BugsVHostBreadcrumb for IPerson
    * The Person bug views must provide:
      * A terse page_title to create a proper page title and breadcrumb.
      * An informative label that explains the purpose the page as the <h1>


QA


    * Visit https://bugs.qastaging.launchpad.net/launchpad/+subscribe
    * Verify the breascrumbs read
      Launchpad itself >> Bugs >> Subscribe
    * Verify the page title is
      Subscribe : Bugs : Launchpad itself

    * Visit https://bugs.qastaging.launchpad.net/launchpad/+bug/533044
    * Verify that the page title is
      Bug #533044 : Bugs : Launchpad itself

    * Visit https://bugs.qastaging.launchpad.net/launchpad/+bug/a77
    * Verify that the page title is
      Error: Page not found

    * Visit https://bugs.qastaging.launchpad.net/~launchpad
    * Verify the page title is
      Bugs : "Canonical Launchpad Engineering" team
    * Verify the breadcrumbs are
      "Canonical Launchpad Engineering" team >> Bugs
    * Verify the first heading is 
      "Canonical Launchpad Engineering" team

    * Visit https://bugs.qastaging.launchpad.net/~launchpad/+assignedbugs
    * Verify the page title is
      Assigned bugs : Bugs : "Canonical Launchpad Engineering" team
    * Verify the breadcrumbs are
      "Canonical Launchpad Engineering" team >> Bugs >> Assigned bugs

    * Visit https://qastaging.launchpad.net/~launchpad
    * Verify the heading is "Canonical Launchpad Engineering" team

    * Visit https://answers.qastaging.launchpad.net/~launchpad
    * Verify the first heading is "Canonical Launchpad Engineering" team

    * Visit https://code.qastaging.launchpad.net/~launchpad
    * Verify the first heading is "Canonical Launchpad Engineering" team

    * Visit https://blueprints.qastaging.launchpad.net/~launchpad
    * Verify the first heading is "Canonical Launchpad Engineering" team

    * Visit https://translations.qastaging.launchpad.net/~launchpad
    * Verify the first heading is "Canonical Launchpad Engineering" team



LINT

    lib/lp/app/browser/tales.py
    lib/lp/bugs/browser/bugtask.py
    lib/lp/bugs/browser/structuralsubscription.py
    lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt
    lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt
    lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt
    lib/lp/bugs/stories/bugs/xx-bug-create-question.txt
    lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
    lib/lp/bugs/stories/bugtask-searches/xx-advanced-upstream-pending-bugwatch.txt
    lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt
    lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt
    lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
    lib/lp/services/webapp/error.py
    lib/lp/bugs/browser/configure.zcml
    lib/lp/bugs/browser/tests/test_breadcrumbs.py
    ^ Lint is not happy with some of the stories. I can fix these after the
      review.

TEST

    ./bin/test -vvc -t xx-also-affects-new-upstream \
        -t xx-attachments-to-bug-report -t xx-bug-comments-truncated \
        -t xx-bug-create-question -t xx-bug-obfuscation \
        -t xx-advanced-upstream-pending-bugwatch \
        -t xx-filebug-attachments -t xx-product-guided-filebug \
        -t xx-project-guided-filebug lp.bugs.tests.test_doc
    ./bin/test -vvc lp.bugs.browser.tests.test_breadcrumbs
    ./bin/test -vvc -t test_title lp.registry.tests.test_person



IMPLEMENTATION

I replaced the check for override_title_breadcrumb in ObjectFormatterAPI
to instead check if the view is an instance of SystemErrorView. I
removed all override_title_breadcrumb attributes from the error views
and the two offending bugs views. I updated many tests, and it is clear
that the ellipsis in the stories was hiding the title insanity.
    lib/lp/app/browser/tales.py
    lib/lp/bugs/browser/bugtask.py
    lib/lp/bugs/browser/structuralsubscription.py
    lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt
    lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt
    lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt
    lib/lp/bugs/stories/bugs/xx-bug-create-question.txt
    lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
    lib/lp/bugs/stories/bugtask-searches/xx-advanced-upstream-pending-bugwatch.txt
    lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt
    lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt
    lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
    lib/lp/services/webapp/error.py

Bug #516485 is caused by a broken breadcrumb adaption.
BugsVHostBreadcrumb was not registered for IPerson. Adding it made
user/team page titles and breadcrumbs behave like projects and distros.
I added a test to verify the breadcrumb adapter makes the expected crumb.
    lib/lp/bugs/browser/configure.zcml
    lib/lp/bugs/browser/tests/test_breadcrumbs.py

Bug 928234 is solved by ensuring that team.title returns the smartquoted
displayname with team appended to it. I added a test, but I believe there
will be some test failures that I will need to follow up on. I may also
be able to remove numerous calls to smartquote in views now that .title
provides the definitive formatting.
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py
-- 
https://code.launchpad.net/~sinzui/launchpad/team-titles/+merge/93675
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/team-titles into lp:launchpad.
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py	2012-02-15 03:59:49 +0000
+++ lib/lp/app/browser/tales.py	2012-02-18 01:30:26 +0000
@@ -78,6 +78,7 @@
     )
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.canonicalurl import nearest_adapter
+from lp.services.webapp.error import SystemErrorView
 from lp.services.webapp.interfaces import (
     IApplicationMenu,
     IContextMenu,
@@ -671,17 +672,13 @@
 
         By default, reverse breadcrumbs are always used if they are available.
         If not available, then the view's .page_title attribut is used.
-        If breadcrumbs are available, then a view can still choose to
-        override them by setting the attribute .override_title_breadcrumbs
-        to True.
         """
         ROOT_TITLE = 'Launchpad'
         view = self._context
         request = get_current_browser_request()
         hierarchy_view = getMultiAdapter(
             (view.context, request), name='+hierarchy')
-        override = getattr(view, 'override_title_breadcrumbs', False)
-        if (override or
+        if (isinstance(view, SystemErrorView) or
             hierarchy_view is None or
             not hierarchy_view.display_breadcrumbs):
             # The breadcrumbs are either not available or are overridden.  If

=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py	2012-02-17 02:53:38 +0000
+++ lib/lp/bugs/browser/bugtask.py	2012-02-18 01:30:26 +0000
@@ -637,8 +637,6 @@
 class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
     """View class for presenting information about an `IBugTask`."""
 
-    override_title_breadcrumbs = True
-
     def __init__(self, context, request):
         LaunchpadView.__init__(self, context, request)
 
@@ -654,6 +652,10 @@
 
     @property
     def page_title(self):
+        return self.context.bug.id
+
+    @property
+    def label(self):
         heading = 'Bug #%s in %s' % (
             self.context.bug.id, self.context.bugtargetdisplayname)
         title = FormattersAPI(self.context.bug.title).obfuscate_email()

=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml	2012-02-01 15:26:32 +0000
+++ lib/lp/bugs/browser/configure.zcml	2012-02-18 01:30:26 +0000
@@ -227,6 +227,11 @@
             title="Admin"
             action="+admin"/>
     </browser:menuItems>
+    <browser:menus
+        module="lp.bugs.browser.person"
+        classes="
+            PersonBugsMenu
+            "/>
     <browser:defaultView
         for="lp.bugs.interfaces.malone.IMaloneApplication"
         name="+index"/>
@@ -266,6 +271,12 @@
         class="lp.bugs.browser.bug.DeprecatedAssignedBugsView"
         attribute="redirect_to_assignedbugs"
         permission="launchpad.AnyPerson"/>
+    <adapter
+        name="bugs"
+        provides="lp.services.webapp.interfaces.IBreadcrumb"
+        for="lp.registry.interfaces.person.IPerson"
+        factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb"
+        permission="zope.Public"/>
     <browser:page
         for="lp.registry.interfaces.person.IPerson"
         name="+team-bugs-macro"
@@ -273,51 +284,63 @@
         permission="zope.Public"
         class="lp.app.browser.launchpad.Macro"/>
     <browser:page
+        for="lp.registry.interfaces.person.IPerson"
+        permission="zope.Public"
+        class="lp.bugs.browser.person.PersonSubscriptionsView"
+        name="+subscriptions"
+        template="../templates/person-subscriptions.pt"/>
+    <browser:page
+        for="lp.registry.interfaces.person.IPerson"
+        permission="zope.Public"
+        class="lp.bugs.browser.person.PersonStructuralSubscriptionsView"
+        name="+structural-subscriptions"
+        template="../templates/person-structural-subscriptions.pt"/>
+    <browser:page
         name="+bugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonRelatedBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonRelatedBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page
         name="+affectingbugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonAffectingBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonAffectingBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page
         name="+assignedbugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonAssignedBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonAssignedBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page
         name="+commentedbugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonCommentedBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonCommentedBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page
         name="+packagebugs-search"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.BugSubscriberPackageBugsSearchListingView"
+        class="lp.bugs.browser.person.BugSubscriberPackageBugsSearchListingView"
         permission="zope.Public"
         template="../templates/person-packagebugs-search.pt"/>
     <browser:page
         name="+packagebugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.BugSubscriberPackageBugsOverView"
+        class="lp.bugs.browser.person.BugSubscriberPackageBugsOverView"
         permission="zope.Public"
         template="../templates/person-packagebugs-overview.pt"/>
     <browser:page
         name="+reportedbugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonReportedBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonReportedBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page
         name="+subscribedbugs"
         for="lp.registry.interfaces.person.IPerson"
-        class="lp.registry.browser.person.PersonSubscribedBugTaskSearchListingView"
+        class="lp.bugs.browser.person.PersonSubscribedBugTaskSearchListingView"
         permission="zope.Public"
         template="../templates/buglisting-embedded-advanced-search.pt"/>
     <browser:page

=== added file 'lib/lp/bugs/browser/person.py'
--- lib/lp/bugs/browser/person.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/person.py	2012-02-18 01:30:26 +0000
@@ -0,0 +1,758 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""IPerson browser views related to bugs."""
+
+__metaclass__ = type
+
+__all__ = [
+    'BugSubscriberPackageBugsSearchListingView',
+    'PersonBugsMenu',
+    'PersonCommentedBugTaskSearchListingView',
+    'PersonAssignedBugTaskSearchListingView',
+    'PersonRelatedBugTaskSearchListingView',
+    'PersonReportedBugTaskSearchListingView',
+    'PersonStructuralSubscriptionsView',
+    'PersonSubscribedBugTaskSearchListingView',
+    'PersonSubscriptionsView',
+    ]
+
+import copy
+from operator import itemgetter
+import urllib
+
+from storm.expr import Join
+
+from zope.component import getUtility
+from zope.schema.vocabulary import getVocabularyRegistry
+
+from lp.app.errors import UnexpectedFormData
+from lp.bugs.browser.bugtask import BugTaskSearchListingView
+from lp.bugs.interfaces.bugtask import (
+    BugTaskStatus,
+    IBugTaskSet,
+    UNRESOLVED_BUGTASK_STATUSES,
+    )
+from lp.bugs.model.bugtask import BugTask
+from lp.registry.model.milestone import (
+    Milestone,
+    milestone_sort_key,
+    )
+from lp.registry.interfaces.person import IPerson
+from lp.services.feeds.browser import FeedsMixin
+from lp.services.helpers import shortlist
+from lp.services.propertycache import cachedproperty
+from lp.services.webapp.menu import  (
+    Link,
+    NavigationMenu,
+    )
+from lp.services.webapp.batching import BatchNavigator
+from lp.services.webapp.publisher import (
+    canonical_url,
+    LaunchpadView,
+    )
+
+
+def get_package_search_url(distributionsourcepackage, person_url,
+                           advanced=False, extra_params=None):
+    """Construct a default search URL for a distributionsourcepackage.
+
+    Optional filter parameters can be specified as a dict with the
+    extra_params argument.
+    """
+    params = {
+        "field.distribution": distributionsourcepackage.distribution.name,
+        "field.sourcepackagename": distributionsourcepackage.name,
+        "search": "Search"}
+    if advanced:
+        params['advanced'] = '1'
+
+    if extra_params is not None:
+        # We must UTF-8 encode searchtext to play nicely with
+        # urllib.urlencode, because it may contain non-ASCII characters.
+        if 'field.searchtext' in extra_params:
+            extra_params["field.searchtext"] = (
+                extra_params["field.searchtext"].encode("utf8"))
+
+        params.update(extra_params)
+
+    query_string = urllib.urlencode(sorted(params.items()), doseq=True)
+
+    return person_url + '/+packagebugs-search?%s' % query_string
+
+
+class PersonBugsMenu(NavigationMenu):
+
+    usedfor = IPerson
+    facet = 'bugs'
+    links = ['affectingbugs', 'assignedbugs', 'commentedbugs', 'reportedbugs',
+             'subscribedbugs', 'relatedbugs', 'softwarebugs']
+
+    def relatedbugs(self):
+        text = 'All related bugs'
+        summary = ('All bug reports which %s reported, is assigned to, '
+                   'or is subscribed to.' % self.context.displayname)
+        return Link('', text, site='bugs', summary=summary)
+
+    def assignedbugs(self):
+        text = 'Assigned bugs'
+        summary = 'Bugs assigned to %s.' % self.context.displayname
+        return Link('+assignedbugs', text, site='bugs', summary=summary)
+
+    def softwarebugs(self):
+        text = 'Subscribed packages'
+        summary = (
+            'A summary report for packages where %s is a subscriber.'
+            % self.context.displayname)
+        return Link('+packagebugs', text, site='bugs', summary=summary)
+
+    def reportedbugs(self):
+        text = 'Reported bugs'
+        summary = 'Bugs reported by %s.' % self.context.displayname
+        enabled = not self.context.is_team
+        return Link(
+            '+reportedbugs', text, site='bugs', summary=summary,
+            enabled=enabled)
+
+    def subscribedbugs(self):
+        text = 'Subscribed bugs'
+        summary = ('Bug reports %s is subscribed to.'
+                   % self.context.displayname)
+        return Link('+subscribedbugs', text, site='bugs', summary=summary)
+
+    def commentedbugs(self):
+        text = 'Commented bugs'
+        summary = ('Bug reports on which %s has commented.'
+                   % self.context.displayname)
+        enabled = not self.context.is_team
+        return Link(
+            '+commentedbugs', text, site='bugs', summary=summary,
+            enabled=enabled)
+
+    def affectingbugs(self):
+        text = 'Affecting bugs'
+        summary = ('Bugs affecting %s.' % self.context.displayname)
+        enabled = not self.context.is_team
+        return Link(
+            '+affectingbugs', text, site='bugs', summary=summary,
+            enabled=enabled)
+
+
+class RelevantMilestonesMixin:
+    """Mixin to narrow the milestone list to only relevant milestones."""
+
+    def getMilestoneWidgetValues(self):
+        """Return data used to render the milestone checkboxes."""
+        prejoins = [
+            (Milestone, Join(Milestone, BugTask.milestone == Milestone.id))]
+        milestones = [
+            bugtask.milestone
+            for bugtask in self.searchUnbatched(prejoins=prejoins)]
+        milestones = sorted(milestones, key=milestone_sort_key, reverse=True)
+        return [
+            dict(title=milestone.title, value=milestone.id, checked=False)
+            for milestone in milestones]
+
+
+class BugSubscriberPackageBugsOverView(LaunchpadView):
+
+    page_title = 'Package bugs'
+
+    @cachedproperty
+    def total_bug_counts(self):
+        """Return the totals of each type of package bug count as a dict."""
+        totals = {
+            'open_bugs_count': 0,
+            'critical_bugs_count': 0,
+            'high_bugs_count': 0,
+            'unassigned_bugs_count': 0,
+            'inprogress_bugs_count': 0,
+            }
+
+        for package_counts in self.package_bug_counts:
+            for key in totals.keys():
+                totals[key] += int(package_counts[key])
+
+        return totals
+
+    @cachedproperty
+    def package_bug_counts(self):
+        """Return a list of dicts used for rendering package bug counts."""
+        L = []
+        package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
+            self.user, self.context.getBugSubscriberPackages())
+        person_url = canonical_url(self.context)
+        for package_counts in package_counts:
+            package = package_counts['package']
+            L.append({
+                'package_name': package.displayname,
+                'package_search_url':
+                    get_package_search_url(package, person_url),
+                'open_bugs_count': package_counts['open'],
+                'open_bugs_url': self.getOpenBugsURL(package, person_url),
+                'critical_bugs_count': package_counts['open_critical'],
+                'critical_bugs_url': self.getCriticalBugsURL(
+                    package, person_url),
+                'high_bugs_count': package_counts['open_high'],
+                'high_bugs_url': self.getHighBugsURL(package, person_url),
+                'unassigned_bugs_count': package_counts['open_unassigned'],
+                'unassigned_bugs_url': self.getUnassignedBugsURL(
+                    package, person_url),
+                'inprogress_bugs_count': package_counts['open_inprogress'],
+                'inprogress_bugs_url': self.getInProgressBugsURL(
+                    package, person_url),
+            })
+
+        return sorted(L, key=itemgetter('package_name'))
+
+    def getOpenBugsURL(self, distributionsourcepackage, person_url):
+        """Return the URL for open bugs on distributionsourcepackage."""
+        status_params = {'field.status': []}
+
+        for status in UNRESOLVED_BUGTASK_STATUSES:
+            status_params['field.status'].append(status.title)
+
+        return get_package_search_url(
+            distributionsourcepackage=distributionsourcepackage,
+            person_url=person_url,
+            extra_params=status_params)
+
+    def getCriticalBugsURL(self, distributionsourcepackage, person_url):
+        """Return the URL for critical bugs on distributionsourcepackage."""
+        critical_bugs_params = {
+            'field.status': [], 'field.importance': "Critical"}
+
+        for status in UNRESOLVED_BUGTASK_STATUSES:
+            critical_bugs_params["field.status"].append(status.title)
+
+        return get_package_search_url(
+            distributionsourcepackage=distributionsourcepackage,
+            person_url=person_url,
+            extra_params=critical_bugs_params)
+
+    def getHighBugsURL(self, distributionsourcepackage, person_url):
+        """Return URL for high bugs on distributionsourcepackage."""
+        high_bugs_params = {
+            'field.status': [], 'field.importance': "High"}
+
+        for status in UNRESOLVED_BUGTASK_STATUSES:
+            high_bugs_params["field.status"].append(status.title)
+
+        return get_package_search_url(
+            distributionsourcepackage=distributionsourcepackage,
+            person_url=person_url,
+            extra_params=high_bugs_params)
+
+    def getUnassignedBugsURL(self, distributionsourcepackage, person_url):
+        """Return the URL for unassigned bugs on distributionsourcepackage."""
+        unassigned_bugs_params = {
+            "field.status": [], "field.unassigned": "on"}
+
+        for status in UNRESOLVED_BUGTASK_STATUSES:
+            unassigned_bugs_params["field.status"].append(status.title)
+
+        return get_package_search_url(
+            distributionsourcepackage=distributionsourcepackage,
+            person_url=person_url,
+            extra_params=unassigned_bugs_params)
+
+    def getInProgressBugsURL(self, distributionsourcepackage, person_url):
+        """Return the URL for unassigned bugs on distributionsourcepackage."""
+        inprogress_bugs_params = {"field.status": "In Progress"}
+
+        return get_package_search_url(
+            distributionsourcepackage=distributionsourcepackage,
+            person_url=person_url,
+            extra_params=inprogress_bugs_params)
+
+
+class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
+    """Bugs reported on packages for a bug subscriber."""
+
+    columns_to_show = ["id", "summary", "importance", "status"]
+    page_title = 'Package bugs'
+
+    @property
+    def current_package(self):
+        """Get the package whose bugs are currently being searched."""
+        if not (
+            self.widgets['distribution'].hasValidInput() and
+            self.widgets['distribution'].getInputValue()):
+            raise UnexpectedFormData("A distribution is required")
+        if not (
+            self.widgets['sourcepackagename'].hasValidInput() and
+            self.widgets['sourcepackagename'].getInputValue()):
+            raise UnexpectedFormData("A sourcepackagename is required")
+
+        distribution = self.widgets['distribution'].getInputValue()
+        return distribution.getSourcePackage(
+            self.widgets['sourcepackagename'].getInputValue())
+
+    def search(self, searchtext=None):
+        distrosourcepackage = self.current_package
+        return BugTaskSearchListingView.search(
+            self, searchtext=searchtext, context=distrosourcepackage)
+
+    def getMilestoneWidgetValues(self):
+        """See `BugTaskSearchListingView`.
+
+        We return only the active milestones on the current distribution
+        since any others are irrelevant.
+        """
+        current_distro = self.current_package.distribution
+        vocabulary_registry = getVocabularyRegistry()
+        vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
+
+        return shortlist([
+            dict(title=milestone.title, value=milestone.token, checked=False)
+            for milestone in vocabulary],
+            longest_expected=10)
+
+    @cachedproperty
+    def person_url(self):
+        return canonical_url(self.context)
+
+    def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
+                                         advanced=False, extra_params=None):
+        """Construct a default search URL for a distributionsourcepackage.
+
+        Optional filter parameters can be specified as a dict with the
+        extra_params argument.
+        """
+        if distributionsourcepackage is None:
+            distributionsourcepackage = self.current_package
+        return get_package_search_url(
+            distributionsourcepackage, self.person_url, advanced,
+            extra_params)
+
+    def getBugSubscriberPackageAdvancedSearchURL(self,
+                                              distributionsourcepackage=None):
+        """Build the advanced search URL for a distributionsourcepackage."""
+        return self.getBugSubscriberPackageSearchURL(advanced=True)
+
+    def shouldShowSearchWidgets(self):
+        # XXX: Guilherme Salgado 2005-11-05:
+        # It's not possible to search amongst the bugs on maintained
+        # software, so for now I'll be simply hiding the search widgets.
+        return False
+
+    # Methods that customize the advanced search form.
+    def getAdvancedSearchButtonLabel(self):
+        return "Search bugs in %s" % self.current_package.displayname
+
+    def getSimpleSearchURL(self):
+        return get_package_search_url(self.current_package, self.person_url)
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return ("in %s related to %s" %
+                (self.current_package.displayname, self.context.displayname))
+
+
+class PersonAssignedBugTaskSearchListingView(RelevantMilestonesMixin,
+                                             BugTaskSearchListingView):
+    """All bugs assigned to someone."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    page_title = 'Assigned bugs'
+    view_name = '+assignedbugs'
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the open bugs assigned to a person."""
+        if context is None:
+            context = self.context
+
+        if extra_params is None:
+            extra_params = dict()
+        else:
+            extra_params = dict(extra_params)
+        extra_params['assignee'] = context
+
+        sup = super(PersonAssignedBugTaskSearchListingView, self)
+        return sup.searchUnbatched(
+            searchtext, context, extra_params, prejoins)
+
+    def shouldShowAssigneeWidget(self):
+        """Should the assignee widget be shown on the advanced search page?"""
+        return False
+
+    def shouldShowTeamPortlet(self):
+        """Should the team assigned bugs portlet be shown?"""
+        return True
+
+    def shouldShowTagsCombinatorWidget(self):
+        """Should the tags combinator widget show on the search page?"""
+        return False
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "assigned to %s" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        """The header for the search page."""
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        """The Search button for the advanced search page."""
+        return "Search bugs %s" % self.context_description
+
+    def getSimpleSearchURL(self):
+        """Return a URL that can be used as an href to the simple search."""
+        return canonical_url(self.context, view_name="+assignedbugs")
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonCommentedBugTaskSearchListingView(RelevantMilestonesMixin,
+                                              BugTaskSearchListingView):
+    """All bugs commented on by a Person."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    page_title = 'Commented bugs'
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the open bugs commented on by a person."""
+        if context is None:
+            context = self.context
+
+        if extra_params is None:
+            extra_params = dict()
+        else:
+            extra_params = dict(extra_params)
+        extra_params['bug_commenter'] = context
+
+        sup = super(PersonCommentedBugTaskSearchListingView, self)
+        return sup.searchUnbatched(
+            searchtext, context, extra_params, prejoins)
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "commented on by %s" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        """The header for the search page."""
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        """The Search button for the advanced search page."""
+        return "Search bugs %s" % self.context_description
+
+    def getSimpleSearchURL(self):
+        """Return a URL that can be used as an href to the simple search."""
+        return canonical_url(self.context, view_name="+commentedbugs")
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonAffectingBugTaskSearchListingView(
+    RelevantMilestonesMixin, BugTaskSearchListingView):
+    """All bugs affecting someone."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    view_name = '+affectingbugs'
+    page_title = 'Bugs affecting'   # The context is added externally.
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the open bugs assigned to a person."""
+        if context is None:
+            context = self.context
+
+        if extra_params is None:
+            extra_params = dict()
+        else:
+            extra_params = dict(extra_params)
+        extra_params['affected_user'] = context
+
+        sup = super(PersonAffectingBugTaskSearchListingView, self)
+        return sup.searchUnbatched(
+            searchtext, context, extra_params, prejoins)
+
+    def shouldShowAssigneeWidget(self):
+        """Should the assignee widget be shown on the advanced search page?"""
+        return False
+
+    def shouldShowTeamPortlet(self):
+        """Should the team assigned bugs portlet be shown?"""
+        return True
+
+    def shouldShowTagsCombinatorWidget(self):
+        """Should the tags combinator widget show on the search page?"""
+        return False
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "affecting %s" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        """The header for the search page."""
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        """The Search button for the advanced search page."""
+        return "Search bugs %s" % self.context_description
+
+    def getSimpleSearchURL(self):
+        """Return a URL that can be used as an href to the simple search."""
+        return canonical_url(self.context, view_name=self.view_name)
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonRelatedBugTaskSearchListingView(RelevantMilestonesMixin,
+                                            BugTaskSearchListingView,
+                                            FeedsMixin):
+    """All bugs related to someone."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    page_title = 'Related bugs'
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the open bugs related to a person.
+
+        :param extra_params: A dict that provides search params added to
+            the search criteria taken from the request. Params in
+            `extra_params` take precedence over request params.
+        """
+        if context is None:
+            context = self.context
+
+        params = self.buildSearchParams(extra_params=extra_params)
+        subscriber_params = copy.copy(params)
+        subscriber_params.subscriber = context
+        assignee_params = copy.copy(params)
+        owner_params = copy.copy(params)
+        commenter_params = copy.copy(params)
+
+        # Only override the assignee, commenter and owner if they were not
+        # specified by the user.
+        if assignee_params.assignee is None:
+            assignee_params.assignee = context
+        if owner_params.owner is None:
+            # Specify both owner and bug_reporter to try to prevent the same
+            # bug (but different tasks) being displayed.
+            owner_params.owner = context
+            owner_params.bug_reporter = context
+        if commenter_params.bug_commenter is None:
+            commenter_params.bug_commenter = context
+
+        return context.searchTasks(
+            assignee_params, subscriber_params, owner_params,
+            commenter_params, prejoins=prejoins)
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "related to %s" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        return "Search bugs %s" % self.context_description
+
+    def getSimpleSearchURL(self):
+        return canonical_url(self.context, view_name="+bugs")
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonReportedBugTaskSearchListingView(RelevantMilestonesMixin,
+                                             BugTaskSearchListingView):
+    """All bugs reported by someone."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    page_title = 'Reported bugs'
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the bugs reported by a person."""
+        if context is None:
+            context = self.context
+
+        if extra_params is None:
+            extra_params = dict()
+        else:
+            extra_params = dict(extra_params)
+        # Specify both owner and bug_reporter to try to prevent the same
+        # bug (but different tasks) being displayed.
+        extra_params['owner'] = context
+        extra_params['bug_reporter'] = context
+
+        sup = super(PersonReportedBugTaskSearchListingView, self)
+        return sup.searchUnbatched(
+            searchtext, context, extra_params, prejoins)
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "reported by %s" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        """The header for the search page."""
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        """The Search button for the advanced search page."""
+        return "Search bugs %s" % self.context_description
+
+    def getSimpleSearchURL(self):
+        """Return a URL that can be used as an href to the simple search."""
+        return canonical_url(self.context, view_name="+reportedbugs")
+
+    def shouldShowReporterWidget(self):
+        """Should the reporter widget be shown on the advanced search page?"""
+        return False
+
+    def shouldShowTagsCombinatorWidget(self):
+        """Should the tags combinator widget show on the search page?"""
+        return False
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonSubscribedBugTaskSearchListingView(RelevantMilestonesMixin,
+                                               BugTaskSearchListingView):
+    """All bugs someone is subscribed to."""
+
+    columns_to_show = ["id", "summary", "bugtargetdisplayname",
+                       "importance", "status"]
+    page_title = 'Subscribed bugs'
+    view_name = '+subscribedbugs'
+
+    def searchUnbatched(self, searchtext=None, context=None,
+                        extra_params=None, prejoins=[]):
+        """Return the bugs subscribed to by a person."""
+        if context is None:
+            context = self.context
+
+        if extra_params is None:
+            extra_params = dict()
+        else:
+            extra_params = dict(extra_params)
+        extra_params['subscriber'] = context
+
+        sup = super(PersonSubscribedBugTaskSearchListingView, self)
+        return sup.searchUnbatched(
+            searchtext, context, extra_params, prejoins)
+
+    def shouldShowTeamPortlet(self):
+        """Should the team subscribed bugs portlet be shown?"""
+        return True
+
+    @property
+    def context_description(self):
+        """See `BugTaskSearchListingView`."""
+        return "%s is subscribed to" % self.context.displayname
+
+    def getSearchPageHeading(self):
+        """The header for the search page."""
+        return "Bugs %s" % self.context_description
+
+    def getAdvancedSearchButtonLabel(self):
+        """The Search button for the advanced search page."""
+        return "Search bugs %s is Cc'd to" % self.context.displayname
+
+    def getSimpleSearchURL(self):
+        """Return a URL that can be used as an href to the simple search."""
+        return canonical_url(self.context, view_name="+subscribedbugs")
+
+    @property
+    def label(self):
+        return self.getSearchPageHeading()
+
+
+class PersonSubscriptionsView(LaunchpadView):
+    """All the subscriptions for a person."""
+
+    page_title = 'Subscriptions'
+
+    def subscribedBugTasks(self):
+        """
+        Return a BatchNavigator for distinct bug tasks to which the person is
+        subscribed.
+        """
+        bug_tasks = self.context.searchTasks(None, user=self.user,
+            order_by='-date_last_updated',
+            status=(BugTaskStatus.NEW,
+                    BugTaskStatus.INCOMPLETE,
+                    BugTaskStatus.CONFIRMED,
+                    BugTaskStatus.TRIAGED,
+                    BugTaskStatus.INPROGRESS,
+                    BugTaskStatus.FIXCOMMITTED,
+                    BugTaskStatus.INVALID),
+            bug_subscriber=self.context)
+
+        sub_bug_tasks = []
+        sub_bugs = set()
+
+        # XXX: GavinPanella 2010-10-08 bug=656904: This materializes the
+        # entire result set. It would probably be more efficient implemented
+        # with a pre_iter_hook on a DecoratedResultSet.
+        for task in bug_tasks:
+            # We order the bugtasks by date_last_updated but we always display
+            # the default task for the bug. This is to avoid ordering issues
+            # in tests and also prevents user confusion (because nothing is
+            # more confusing than your subscription targets changing seemingly
+            # at random).
+            if task.bug not in sub_bugs:
+                # XXX: GavinPanella 2010-10-08 bug=656904: default_bugtask
+                # causes a query to be executed. It would be more efficient to
+                # get the default bugtask in bulk, in a pre_iter_hook on a
+                # DecoratedResultSet perhaps.
+                sub_bug_tasks.append(task.bug.default_bugtask)
+                sub_bugs.add(task.bug)
+
+        return BatchNavigator(sub_bug_tasks, self.request)
+
+    def canUnsubscribeFromBugTasks(self):
+        """Can the current user unsubscribe from the bug tasks shown?"""
+        return (self.user is not None and
+                self.user.inTeam(self.context))
+
+    @property
+    def label(self):
+        """The header for the subscriptions page."""
+        return "Subscriptions for %s" % self.context.displayname
+
+
+class PersonStructuralSubscriptionsView(LaunchpadView):
+    """All the structural subscriptions for a person."""
+
+    page_title = 'Structural subscriptions'
+
+    def canUnsubscribeFromBugTasks(self):
+        """Can the current user modify subscriptions for the context?"""
+        return (self.user is not None and
+                self.user.inTeam(self.context))
+
+    @property
+    def label(self):
+        """The header for the structural subscriptions page."""
+        return "Structural subscriptions for %s" % self.context.displayname

=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py	2012-02-18 01:30:26 +0000
@@ -95,17 +95,13 @@
     custom_widget('subscriptions_team', LabeledMultiCheckBoxWidget)
     custom_widget('remove_other_subscriptions', LabeledMultiCheckBoxWidget)
 
-    override_title_breadcrumbs = True
+    page_title = 'Subscribe'
 
     @property
-    def page_title(self):
+    def label(self):
         return 'Subscribe to Bugs in %s' % self.context.title
 
     @property
-    def label(self):
-        return self.page_title
-
-    @property
     def next_url(self):
         return canonical_url(self.context)
 

=== modified file 'lib/lp/bugs/browser/tests/test_breadcrumbs.py'
--- lib/lp/bugs/browser/tests/test_breadcrumbs.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/browser/tests/test_breadcrumbs.py	2012-02-18 01:30:26 +0000
@@ -77,3 +77,22 @@
             (self.bug_tracker.title, self.bug_tracker_url),
             ]
         self.assertBreadcrumbs(expected_breadcrumbs, self.bug_tracker)
+
+
+class BugsVHostBreadcrumbTestCase(BaseBreadcrumbTestCase):
+
+    def test_person(self):
+        person = self.factory.makePerson(name='snarf')
+        person_bugs_url = canonical_url(person, rootsite='bugs')
+        crumbs = self.getBreadcrumbsForObject(person, rootsite='bugs')
+        last_crumb = crumbs[-1]
+        self.assertEquals(person_bugs_url, last_crumb.url)
+        self.assertEquals("Bugs", last_crumb.text)
+
+    def test_bugtarget(self):
+        project = self.factory.makeProduct(name='fnord')
+        project_bugs_url = canonical_url(project, rootsite='bugs')
+        crumbs = self.getBreadcrumbsForObject(project, rootsite='bugs')
+        last_crumb = crumbs[-1]
+        self.assertEquals(project_bugs_url, last_crumb.url)
+        self.assertEquals("Bugs", last_crumb.text)

=== modified file 'lib/lp/bugs/browser/tests/test_person_bugs.py'
--- lib/lp/bugs/browser/tests/test_person_bugs.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/browser/tests/test_person_bugs.py	2012-02-18 01:30:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version (see the file LICENSE).
 
 """Unit tests for person bug views."""
@@ -6,8 +6,9 @@
 __metaclass__ = type
 
 from lp.app.errors import UnexpectedFormData
+from lp.app.browser.tales import MenuAPI
+from lp.bugs.browser import person
 from lp.bugs.interfaces.bugtask import BugTaskStatus
-from lp.registry.browser import person
 from lp.testing import (
     person_logged_in,
     TestCaseWithFactory,
@@ -17,6 +18,34 @@
 from lp.testing.views import create_initialized_view
 
 
+class PersonBugsMenuTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_user(self):
+        user = self.factory.makePerson()
+        menu_api = MenuAPI(user)
+        menu_api._selectedfacetname = 'bugs'
+        enabled_links = sorted(
+            link.name for link in menu_api.navigation.values()
+            if link.enabled)
+        expected_links = [
+            'affectingbugs', 'assignedbugs', 'commentedbugs',
+            'relatedbugs', 'reportedbugs', 'softwarebugs', 'subscribedbugs']
+        self.assertEqual(expected_links, enabled_links)
+
+    def test_team(self):
+        team = self.factory.makeTeam()
+        menu_api = MenuAPI(team)
+        menu_api._selectedfacetname = 'bugs'
+        enabled_links = sorted(
+            link.name for link in menu_api.navigation.values()
+            if link.enabled)
+        expected_links = [
+            'assignedbugs', 'relatedbugs', 'softwarebugs', 'subscribedbugs']
+        self.assertEqual(expected_links, enabled_links)
+
+
 class TestBugSubscriberPackageBugsSearchListingView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer

=== modified file 'lib/lp/bugs/feed/bug.py'
--- lib/lp/bugs/feed/bug.py	2011-12-30 07:23:45 +0000
+++ lib/lp/bugs/feed/bug.py	2012-02-18 01:30:26 +0000
@@ -19,6 +19,7 @@
     BugsBugTaskSearchListingView,
     BugTargetView,
     )
+from lp.bugs.browser.person import PersonRelatedBugTaskSearchListingView
 from lp.bugs.interfaces.bug import (
     IBug,
     IBugSet,
@@ -26,7 +27,6 @@
 from lp.bugs.interfaces.bugtarget import IHasBugs
 from lp.bugs.interfaces.bugtask import IBugTaskSet
 from lp.bugs.interfaces.malone import IMaloneApplication
-from lp.registry.browser.person import PersonRelatedBugTaskSearchListingView
 from lp.registry.interfaces.person import IPerson
 from lp.services.config import config
 from lp.services.feeds.feed import (
@@ -77,11 +77,11 @@
         different feeds.
         """
         self.show_column = dict(
-            id = True,
-            title = True,
-            bugtargetdisplayname = True,
-            importance = True,
-            status = True)
+            id=True,
+            title=True,
+            bugtargetdisplayname=True,
+            importance=True,
+            status=True)
 
     @property
     def logo(self):

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt	2011-12-22 05:09:10 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt	2012-02-18 01:30:26 +0000
@@ -19,7 +19,7 @@
 We're now redirected to the newly created bugtask page.
 
     >>> user_browser.title
-    'Bug #1 in The Foo Project...'
+    'Bug #1 : Bugs : The Foo Project'
 
 When creating a new upstream through this page we'll check if there's any
 upstream already registered in Launchpad which uses the same bugtracker as
@@ -61,7 +61,7 @@
 
     >>> user_browser.getControl('Use Existing Project').click()
     >>> user_browser.title
-    'Bug #2 in The Foo Project...'
+    'Bug #2 (blackhole) : Bugs : The Foo Project'
 
     >>> from lp.bugs.tests.bug import print_remote_bugtasks
     >>> print_remote_bugtasks(user_browser.contents)
@@ -86,7 +86,7 @@
     ...     'http://bugs.foo.org/bugs/show_bug.cgi?id=123')
     >>> user_browser.getControl('Continue').click()
     >>> user_browser.title
-    'Bug #2 in The Bar Project:...
+    'Bug #2 (blackhole) : Bugs : The Bar Project'
     >>> print_remote_bugtasks(user_browser.contents)
     The Bar Project ...   auto-bugs.foo.org #123
     The Bar Project ...   auto-bugs.foo.org #421

=== modified file 'lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt'
--- lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt	2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt	2012-02-18 01:30:26 +0000
@@ -72,7 +72,7 @@
     >>> logout()
     >>> browser.open("http://bugs.launchpad.dev/redfish/+bug/11";)
     >>> print extract_text(browser.contents)
-    Bug #11 in Jokosher...
+    Bug #11 : ...
     ...Patches...
     ...a patch...
     ...Bug attachments...

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2012-02-01 15:26:32 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2012-02-18 01:30:26 +0000
@@ -87,7 +87,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/tomcat/+bug/2')
     >>> user_browser.title.decode('utf-8')
-    u'Bug #2 in Tomcat: ...Blackhole Trash folder...'
+    u'Bug #2 (blackhole) : Bugs : Tomcat'
     >>> user_browser.getControl(name='field.comment').value = (
     ...     "-----BEGIN PGP SIGNED MESSAGE-----\n"
     ...     "Hash: SHA1\n"
@@ -120,7 +120,7 @@
 email addresses in messages.
 
     >>> user_browser.title.decode('utf-8')
-    u'Bug #2 in Tomcat: ...Blackhole Trash folder...'
+    u'Bug #2 (blackhole) : Bugs : Tomcat'
     >>> text = find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]
     >>> print extract_text(text.findAll('p')[-2])
@@ -135,7 +135,7 @@
 
     >>> anon_browser.open('http://bugs.launchpad.dev/tomcat/+bug/2')
     >>> anon_browser.title.decode('utf-8')
-    u'Bug #2 in Tomcat: ...Blackhole Trash folder...'
+    u'Bug #2 (blackhole) : Bugs : Tomcat'
     >>> text = find_tags_by_class(
     ...     anon_browser.contents, 'boardCommentBody')[-1]
     >>> print extract_text(text.findAll('p')[-2])

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-create-question.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-create-question.txt	2011-12-04 03:58:11 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-create-question.txt	2012-02-18 01:30:26 +0000
@@ -14,7 +14,7 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/ubuntu/+source/linux-source-2.6.15/+bug/10')
     >>> anon_browser.title
-    'Bug #10 in linux-source-2.6.15 (Ubuntu): ...another test bug...'
+    'Bug #10 : Bugs : ...linux-source-2.6.15... package : Ubuntu'
 
     >>> anon_browser.getLink('Convert to a question').click()
     Traceback (most recent call last):
@@ -28,7 +28,7 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/ubuntu/+source/linux-source-2.6.15/+bug/10')
     >>> user_browser.title
-    'Bug #10 in linux-source-2.6.15 (Ubuntu): ...another test bug...'
+    'Bug #10 : Bugs : ...linux-source-2.6.15... package : Ubuntu'
 
     >>> user_browser.getLink('Convert to a question').click()
     >>> user_browser.title
@@ -50,7 +50,7 @@
 informational message stating that a question was created from the bug.
 
     >>> user_browser.title
-    'Bug #10 in linux-source-2.6.15 (Ubuntu): ...another test bug...'
+    'Bug #10 : Bugs : ...linux-source-2.6.15... package : Ubuntu'
 
     >>> content = find_main_content(user_browser.contents)
     >>> content.find(id="bug-is-question")
@@ -114,7 +114,7 @@
 
     >>> user_browser.getLink('#10: another test bug').click()
     >>> user_browser.title
-    'Bug #10 in linux-source-2.6.15 (Ubuntu): ...another test bug...'
+    'Bug #10 : Bugs : ...linux-source-2.6.15... package : Ubuntu'
 
 
 When a question cannot be created from a bug
@@ -127,8 +127,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/thunderbird/+bug/9')
     >>> user_browser.title
-    'Bug #9 in Mozilla Thunderbird: \xe2\x80\x9cThunderbird
-    crashes\xe2\x80\x9d'
+    'Bug #9 : ...'
 
     >>> user_browser.getLink('Convert to a question').click()
     >>> print user_browser.title
@@ -191,13 +190,13 @@
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/jokosher/+bug/12')
     >>> user_browser.title
-    'Bug #12 in Jokosher: ...Copy, Cut and Delete operations should work...'
+    'Bug #12 : ...'
 
     >>> user_browser.getLink('Convert to a question').click()
     >>> user_browser.getControl('Comment').value = 'This will succeed.'
     >>> user_browser.getControl('Convert this Bug into a Question').click()
     >>> user_browser.title
-    'Bug #12 in Jokosher: ...Copy, Cut and Delete operations should work...'
+    'Bug #12 : ...'
 
     >>> print "\n".join(get_feedback_messages(user_browser.contents))
     This bug report was converted into a question:...question #16...
@@ -219,8 +218,7 @@
 reactivate a bug report.
 
     >>> user_browser.title
-    'Bug #12 in Jokosher: \xe2\x80\x9cCopy, Cut and Delete operations should
-    work on selections\xe2\x80\x9d'
+    'Bug #12 : Bugs : Jokosher'
 
     >>> user_browser.getLink('Convert back to a bug').click()
     >>> print user_browser.title
@@ -244,7 +242,7 @@
 the Open status.
 
     >>> user_browser.title
-    'Bug #12 in Jokosher: ...Copy, Cut and Delete operations should work...'
+    'Bug #12 : Bugs : Jokosher'
 
     >>> print "\n".join(get_feedback_messages(user_browser.contents))
     Removed Question #...: Copy, Cut and Delete operations should work...

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt	2011-10-16 08:16:47 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt	2012-02-18 01:30:26 +0000
@@ -13,7 +13,7 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/debian/sarge/+source/mozilla-firefox/+bug/3')
     >>> user_browser.title
-    'Bug #3 in mozilla-firefox (Debian Sarge): ...Bug Title Test...'
+    'Bug #3 : ...'
 
     >>> description = find_tag_by_id(
     ...     user_browser.contents, 'edit-description')
@@ -26,7 +26,7 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/debian/sarge/+source/mozilla-firefox/+bug/3')
     >>> print anon_browser.title
-    Bug #3 in mozilla-firefox (Debian Sarge): ...
+    Bug #3 : ...
 
     >>> 'user@xxxxxxxxxx' in anon_browser.contents
     False

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-advanced-upstream-pending-bugwatch.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-advanced-upstream-pending-bugwatch.txt	2012-02-08 00:47:42 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-advanced-upstream-pending-bugwatch.txt	2012-02-18 01:30:26 +0000
@@ -31,7 +31,7 @@
     ...     'I just want to register that it is upstream').selected = True
     >>> browser.getControl('Add to Bug Report').click()
     >>> browser.title
-    'Bug #... in alsa-utils: ...Test Bug 1...'
+    'Bug #... : Bugs : alsa-utils'
 
 Sample Person visits the advanced search page for alsa-utils, and
 chooses to search for all bugs that need to be forwarded upstream.

=== renamed file 'lib/lp/registry/stories/person/xx-person-bugs.txt' => 'lib/lp/bugs/stories/bugtask-searches/xx-person-bugs.txt'
=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt	2011-04-20 14:56:23 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt	2012-02-18 01:30:26 +0000
@@ -30,7 +30,7 @@
     ...     "of the attachment")
     >>> user_browser.getControl('Submit Bug Report').click()
     >>> user_browser.title
-    'Bug #... in Mozilla Firefox...'
+    'Bug #... : Bugs : Mozilla Firefox'
 
 No Privileges Person sees a notice on the bug page stating that the file
 was attached.

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt	2011-05-16 01:53:42 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt	2012-02-18 01:30:26 +0000
@@ -165,7 +165,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/...'
 
-    >>> user_browser.title
-    "Bug #... in Mozilla Firefox: ...Frankenzombulon reanimated..."
+    >>> print user_browser.title
+    Bug #... : Bugs : Mozilla Firefox
 
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt	2011-05-27 19:53:20 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt	2012-02-18 01:30:26 +0000
@@ -37,7 +37,7 @@
     'http://bugs.launchpad.dev/evolution/+bug/...'
 
     >>> user_browser.title
-    'Bug #... in Evolution: ...Evolution crashes...'
+    'Bug #... : Bugs : Evolution'
 
 
 Subscribing to a similar bug
@@ -109,7 +109,7 @@
     'http://bugs.launchpad.dev/evolution/+bug/...'
 
     >>> user_browser.title
-    'Bug #... in Evolution: ...Faznambutron dumps core...'
+    'Bug #... : Bugs : Evolution'
 
 
 Empty ProjectGroups

=== renamed file 'lib/lp/registry/templates/person-structural-subscriptions.pt' => 'lib/lp/bugs/templates/person-structural-subscriptions.pt'
=== renamed file 'lib/lp/registry/templates/person-subscriptions.pt' => 'lib/lp/bugs/templates/person-subscriptions.pt'
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2012-02-13 21:48:25 +0000
+++ lib/lp/registry/browser/configure.zcml	2012-02-18 01:30:26 +0000
@@ -765,7 +765,6 @@
         <browser:menus
             module="lp.registry.browser.person"
             classes="
-                PersonBugsMenu
                 PersonEditNavigationMenu
                 PersonFacets
                 PersonIndexMenu
@@ -1045,18 +1044,6 @@
             permission="launchpad.Edit"
             template="../templates/person-oauth-tokens.pt"/>
         <browser:page
-            for="lp.registry.interfaces.person.IPerson"
-            permission="zope.Public"
-            class="lp.registry.browser.person.PersonSubscriptionsView"
-            name="+subscriptions"
-            template="../templates/person-subscriptions.pt"/>
-        <browser:page
-            for="lp.registry.interfaces.person.IPerson"
-            permission="zope.Public"
-            class="lp.registry.browser.person.PersonStructuralSubscriptionsView"
-            name="+structural-subscriptions"
-            template="../templates/person-structural-subscriptions.pt"/>
-        <browser:page
             for="lp.registry.interfaces.person.ITeam"
             permission="zope.Public"
             class="lp.registry.browser.team.TeamIndexView"

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2012-02-14 15:32:31 +0000
+++ lib/lp/registry/browser/person.py	2012-02-18 01:30:26 +0000
@@ -8,7 +8,6 @@
 __metaclass__ = type
 __all__ = [
     'BeginTeamClaimView',
-    'BugSubscriberPackageBugsSearchListingView',
     'CommonMenuLinks',
     'EmailToPersonView',
     'PeopleSearchView',
@@ -16,11 +15,8 @@
     'PersonAdministerView',
     'PersonAnswerContactForView',
     'PersonAnswersMenu',
-    'PersonAssignedBugTaskSearchListingView',
     'PersonBrandingView',
-    'PersonBugsMenu',
     'PersonCodeOfConductEditView',
-    'PersonCommentedBugTaskSearchListingView',
     'PersonDeactivateAccountView',
     'PersonEditEmailsView',
     'PersonEditHomePageView',
@@ -41,10 +37,8 @@
     'PersonOverviewMenu',
     'PersonRdfContentsView',
     'PersonRdfView',
-    'PersonRelatedBugTaskSearchListingView',
     'PersonRelatedSoftwareView',
     'PersonRenameFormMixin',
-    'PersonReportedBugTaskSearchListingView',
     'PersonSearchQuestionsView',
     'PersonSetActionNavigationMenu',
     'PersonSetContextMenu',
@@ -53,9 +47,6 @@
     'PersonSpecWorkloadTableView',
     'PersonSpecWorkloadView',
     'PersonSpecsMenu',
-    'PersonStructuralSubscriptionsView',
-    'PersonSubscribedBugTaskSearchListingView',
-    'PersonSubscriptionsView',
     'PersonView',
     'PersonVouchersView',
     'PPANavigationMenuMixIn',
@@ -72,7 +63,6 @@
 
 
 import cgi
-import copy
 from datetime import datetime
 import itertools
 from itertools import chain
@@ -90,7 +80,6 @@
 from lazr.restful.utils import smartquote
 from lazr.uri import URI
 import pytz
-from storm.expr import Join
 from storm.zope.interfaces import IResultSet
 from z3c.ptcompat import ViewPageTemplateFile
 from zope.app.form.browser import (
@@ -158,14 +147,11 @@
 from lp.app.widgets.location import LocationWidget
 from lp.blueprints.browser.specificationtarget import HasSpecificationsView
 from lp.blueprints.enums import SpecificationFilter
-from lp.bugs.browser.bugtask import BugTaskSearchListingView
 from lp.bugs.interfaces.bugtask import (
     BugTaskSearchParams,
     BugTaskStatus,
     IBugTaskSet,
-    UNRESOLVED_BUGTASK_STATUSES,
     )
-from lp.bugs.model.bugtask import BugTask
 from lp.buildmaster.enums import BuildStatus
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.code.errors import InvalidNamespace
@@ -214,10 +200,6 @@
     )
 from lp.registry.interfaces.wikiname import IWikiNameSet
 from lp.registry.mail.notification import send_direct_contact_email
-from lp.registry.model.milestone import (
-    Milestone,
-    milestone_sort_key,
-    )
 from lp.registry.model.person import get_recipients
 from lp.services.config import config
 from lp.services.database.sqlbase import flush_database_updates
@@ -228,7 +210,6 @@
     GPGKeyNotFoundError,
     IGPGHandler,
     )
-from lp.services.helpers import shortlist
 from lp.services.identity.interfaces.account import (
     AccountStatus,
     IAccount,
@@ -611,54 +592,6 @@
         return Link('', text, summary)
 
 
-class PersonBugsMenu(NavigationMenu):
-
-    usedfor = IPerson
-    facet = 'bugs'
-    links = ['affectingbugs', 'assignedbugs', 'commentedbugs', 'reportedbugs',
-             'subscribedbugs', 'relatedbugs', 'softwarebugs']
-
-    def relatedbugs(self):
-        text = 'All related bugs'
-        summary = ('All bug reports which %s reported, is assigned to, '
-                   'or is subscribed to.' % self.context.displayname)
-        return Link('', text, site='bugs', summary=summary)
-
-    def assignedbugs(self):
-        text = 'Assigned bugs'
-        summary = 'Bugs assigned to %s.' % self.context.displayname
-        return Link('+assignedbugs', text, site='bugs', summary=summary)
-
-    def softwarebugs(self):
-        text = 'Subscribed packages'
-        summary = (
-            'A summary report for packages where %s is a subscriber.'
-            % self.context.displayname)
-        return Link('+packagebugs', text, site='bugs', summary=summary)
-
-    def reportedbugs(self):
-        text = 'Reported bugs'
-        summary = 'Bugs reported by %s.' % self.context.displayname
-        return Link('+reportedbugs', text, site='bugs', summary=summary)
-
-    def subscribedbugs(self):
-        text = 'Subscribed bugs'
-        summary = ('Bug reports %s is subscribed to.'
-                   % self.context.displayname)
-        return Link('+subscribedbugs', text, site='bugs', summary=summary)
-
-    def commentedbugs(self):
-        text = 'Commented bugs'
-        summary = ('Bug reports on which %s has commented.'
-                   % self.context.displayname)
-        return Link('+commentedbugs', text, site='bugs', summary=summary)
-
-    def affectingbugs(self):
-        text = 'Affecting bugs'
-        summary = ('Bugs affecting %s.' % self.context.displayname)
-        return Link('+affectingbugs', text, site='bugs', summary=summary)
-
-
 class PersonSpecsMenu(NavigationMenu):
 
     usedfor = IPerson
@@ -1431,654 +1364,6 @@
         return self.context.specifications(filter=filter)
 
 
-def get_package_search_url(distributionsourcepackage, person_url,
-                           advanced=False, extra_params=None):
-    """Construct a default search URL for a distributionsourcepackage.
-
-    Optional filter parameters can be specified as a dict with the
-    extra_params argument.
-    """
-    params = {
-        "field.distribution": distributionsourcepackage.distribution.name,
-        "field.sourcepackagename": distributionsourcepackage.name,
-        "search": "Search"}
-    if advanced:
-        params['advanced'] = '1'
-
-    if extra_params is not None:
-        # We must UTF-8 encode searchtext to play nicely with
-        # urllib.urlencode, because it may contain non-ASCII characters.
-        if 'field.searchtext' in extra_params:
-            extra_params["field.searchtext"] = (
-                extra_params["field.searchtext"].encode("utf8"))
-
-        params.update(extra_params)
-
-    query_string = urllib.urlencode(sorted(params.items()), doseq=True)
-
-    return person_url + '/+packagebugs-search?%s' % query_string
-
-
-class BugSubscriberPackageBugsOverView(LaunchpadView):
-
-    page_title = 'Package bugs'
-
-    @cachedproperty
-    def total_bug_counts(self):
-        """Return the totals of each type of package bug count as a dict."""
-        totals = {
-            'open_bugs_count': 0,
-            'critical_bugs_count': 0,
-            'high_bugs_count': 0,
-            'unassigned_bugs_count': 0,
-            'inprogress_bugs_count': 0,
-            }
-
-        for package_counts in self.package_bug_counts:
-            for key in totals.keys():
-                totals[key] += int(package_counts[key])
-
-        return totals
-
-    @cachedproperty
-    def package_bug_counts(self):
-        """Return a list of dicts used for rendering package bug counts."""
-        L = []
-        package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
-            self.user, self.context.getBugSubscriberPackages())
-        person_url = canonical_url(self.context)
-        for package_counts in package_counts:
-            package = package_counts['package']
-            L.append({
-                'package_name': package.displayname,
-                'package_search_url':
-                    get_package_search_url(package, person_url),
-                'open_bugs_count': package_counts['open'],
-                'open_bugs_url': self.getOpenBugsURL(package, person_url),
-                'critical_bugs_count': package_counts['open_critical'],
-                'critical_bugs_url': self.getCriticalBugsURL(
-                    package, person_url),
-                'high_bugs_count': package_counts['open_high'],
-                'high_bugs_url': self.getHighBugsURL(package, person_url),
-                'unassigned_bugs_count': package_counts['open_unassigned'],
-                'unassigned_bugs_url': self.getUnassignedBugsURL(
-                    package, person_url),
-                'inprogress_bugs_count': package_counts['open_inprogress'],
-                'inprogress_bugs_url': self.getInProgressBugsURL(
-                    package, person_url),
-            })
-
-        return sorted(L, key=itemgetter('package_name'))
-
-    def getOpenBugsURL(self, distributionsourcepackage, person_url):
-        """Return the URL for open bugs on distributionsourcepackage."""
-        status_params = {'field.status': []}
-
-        for status in UNRESOLVED_BUGTASK_STATUSES:
-            status_params['field.status'].append(status.title)
-
-        return get_package_search_url(
-            distributionsourcepackage=distributionsourcepackage,
-            person_url=person_url,
-            extra_params=status_params)
-
-    def getCriticalBugsURL(self, distributionsourcepackage, person_url):
-        """Return the URL for critical bugs on distributionsourcepackage."""
-        critical_bugs_params = {
-            'field.status': [], 'field.importance': "Critical"}
-
-        for status in UNRESOLVED_BUGTASK_STATUSES:
-            critical_bugs_params["field.status"].append(status.title)
-
-        return get_package_search_url(
-            distributionsourcepackage=distributionsourcepackage,
-            person_url=person_url,
-            extra_params=critical_bugs_params)
-
-    def getHighBugsURL(self, distributionsourcepackage, person_url):
-        """Return URL for high bugs on distributionsourcepackage."""
-        high_bugs_params = {
-            'field.status': [], 'field.importance': "High"}
-
-        for status in UNRESOLVED_BUGTASK_STATUSES:
-            high_bugs_params["field.status"].append(status.title)
-
-        return get_package_search_url(
-            distributionsourcepackage=distributionsourcepackage,
-            person_url=person_url,
-            extra_params=high_bugs_params)
-
-    def getUnassignedBugsURL(self, distributionsourcepackage, person_url):
-        """Return the URL for unassigned bugs on distributionsourcepackage."""
-        unassigned_bugs_params = {
-            "field.status": [], "field.unassigned": "on"}
-
-        for status in UNRESOLVED_BUGTASK_STATUSES:
-            unassigned_bugs_params["field.status"].append(status.title)
-
-        return get_package_search_url(
-            distributionsourcepackage=distributionsourcepackage,
-            person_url=person_url,
-            extra_params=unassigned_bugs_params)
-
-    def getInProgressBugsURL(self, distributionsourcepackage, person_url):
-        """Return the URL for unassigned bugs on distributionsourcepackage."""
-        inprogress_bugs_params = {"field.status": "In Progress"}
-
-        return get_package_search_url(
-            distributionsourcepackage=distributionsourcepackage,
-            person_url=person_url,
-            extra_params=inprogress_bugs_params)
-
-
-class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
-    """Bugs reported on packages for a bug subscriber."""
-
-    columns_to_show = ["id", "summary", "importance", "status"]
-    page_title = 'Package bugs'
-
-    @property
-    def current_package(self):
-        """Get the package whose bugs are currently being searched."""
-        if not (
-            self.widgets['distribution'].hasValidInput() and
-            self.widgets['distribution'].getInputValue()):
-            raise UnexpectedFormData("A distribution is required")
-        if not (
-            self.widgets['sourcepackagename'].hasValidInput() and
-            self.widgets['sourcepackagename'].getInputValue()):
-            raise UnexpectedFormData("A sourcepackagename is required")
-
-        distribution = self.widgets['distribution'].getInputValue()
-        return distribution.getSourcePackage(
-            self.widgets['sourcepackagename'].getInputValue())
-
-    def search(self, searchtext=None):
-        distrosourcepackage = self.current_package
-        return BugTaskSearchListingView.search(
-            self, searchtext=searchtext, context=distrosourcepackage)
-
-    def getMilestoneWidgetValues(self):
-        """See `BugTaskSearchListingView`.
-
-        We return only the active milestones on the current distribution
-        since any others are irrelevant.
-        """
-        current_distro = self.current_package.distribution
-        vocabulary_registry = getVocabularyRegistry()
-        vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
-
-        return shortlist([
-            dict(title=milestone.title, value=milestone.token, checked=False)
-            for milestone in vocabulary],
-            longest_expected=10)
-
-    @cachedproperty
-    def person_url(self):
-        return canonical_url(self.context)
-
-    def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
-                                         advanced=False, extra_params=None):
-        """Construct a default search URL for a distributionsourcepackage.
-
-        Optional filter parameters can be specified as a dict with the
-        extra_params argument.
-        """
-        if distributionsourcepackage is None:
-            distributionsourcepackage = self.current_package
-        return get_package_search_url(
-            distributionsourcepackage, self.person_url, advanced,
-            extra_params)
-
-    def getBugSubscriberPackageAdvancedSearchURL(self,
-                                              distributionsourcepackage=None):
-        """Build the advanced search URL for a distributionsourcepackage."""
-        return self.getBugSubscriberPackageSearchURL(advanced=True)
-
-    def shouldShowSearchWidgets(self):
-        # XXX: Guilherme Salgado 2005-11-05:
-        # It's not possible to search amongst the bugs on maintained
-        # software, so for now I'll be simply hiding the search widgets.
-        return False
-
-    # Methods that customize the advanced search form.
-    def getAdvancedSearchButtonLabel(self):
-        return "Search bugs in %s" % self.current_package.displayname
-
-    def getSimpleSearchURL(self):
-        return get_package_search_url(self.current_package, self.person_url)
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return ("in %s related to %s" %
-                (self.current_package.displayname, self.context.displayname))
-
-
-class RelevantMilestonesMixin:
-    """Mixin to narrow the milestone list to only relevant milestones."""
-
-    def getMilestoneWidgetValues(self):
-        """Return data used to render the milestone checkboxes."""
-        prejoins = [
-            (Milestone, Join(Milestone, BugTask.milestone == Milestone.id))]
-        milestones = [
-            bugtask.milestone
-            for bugtask in self.searchUnbatched(prejoins=prejoins)]
-        milestones = sorted(milestones, key=milestone_sort_key, reverse=True)
-        return [
-            dict(title=milestone.title, value=milestone.id, checked=False)
-            for milestone in milestones]
-
-
-class PersonRelatedBugTaskSearchListingView(RelevantMilestonesMixin,
-                                            BugTaskSearchListingView,
-                                            FeedsMixin):
-    """All bugs related to someone."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    page_title = 'Related bugs'
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the open bugs related to a person.
-
-        :param extra_params: A dict that provides search params added to
-            the search criteria taken from the request. Params in
-            `extra_params` take precedence over request params.
-        """
-        if context is None:
-            context = self.context
-
-        params = self.buildSearchParams(extra_params=extra_params)
-        subscriber_params = copy.copy(params)
-        subscriber_params.subscriber = context
-        assignee_params = copy.copy(params)
-        owner_params = copy.copy(params)
-        commenter_params = copy.copy(params)
-
-        # Only override the assignee, commenter and owner if they were not
-        # specified by the user.
-        if assignee_params.assignee is None:
-            assignee_params.assignee = context
-        if owner_params.owner is None:
-            # Specify both owner and bug_reporter to try to prevent the same
-            # bug (but different tasks) being displayed.
-            owner_params.owner = context
-            owner_params.bug_reporter = context
-        if commenter_params.bug_commenter is None:
-            commenter_params.bug_commenter = context
-
-        return context.searchTasks(
-            assignee_params, subscriber_params, owner_params,
-            commenter_params, prejoins=prejoins)
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "related to %s" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        return "Search bugs %s" % self.context_description
-
-    def getSimpleSearchURL(self):
-        return canonical_url(self.context, view_name="+bugs")
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonAffectingBugTaskSearchListingView(
-    RelevantMilestonesMixin, BugTaskSearchListingView):
-    """All bugs affecting someone."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    view_name = '+affectingbugs'
-    page_title = 'Bugs affecting'   # The context is added externally.
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the open bugs assigned to a person."""
-        if context is None:
-            context = self.context
-
-        if extra_params is None:
-            extra_params = dict()
-        else:
-            extra_params = dict(extra_params)
-        extra_params['affected_user'] = context
-
-        sup = super(PersonAffectingBugTaskSearchListingView, self)
-        return sup.searchUnbatched(
-            searchtext, context, extra_params, prejoins)
-
-    def shouldShowAssigneeWidget(self):
-        """Should the assignee widget be shown on the advanced search page?"""
-        return False
-
-    def shouldShowTeamPortlet(self):
-        """Should the team assigned bugs portlet be shown?"""
-        return True
-
-    def shouldShowTagsCombinatorWidget(self):
-        """Should the tags combinator widget show on the search page?"""
-        return False
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "affecting %s" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        """The header for the search page."""
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        """The Search button for the advanced search page."""
-        return "Search bugs %s" % self.context_description
-
-    def getSimpleSearchURL(self):
-        """Return a URL that can be used as an href to the simple search."""
-        return canonical_url(self.context, view_name=self.view_name)
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonAssignedBugTaskSearchListingView(RelevantMilestonesMixin,
-                                             BugTaskSearchListingView):
-    """All bugs assigned to someone."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    page_title = 'Assigned bugs'
-    view_name = '+assignedbugs'
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the open bugs assigned to a person."""
-        if context is None:
-            context = self.context
-
-        if extra_params is None:
-            extra_params = dict()
-        else:
-            extra_params = dict(extra_params)
-        extra_params['assignee'] = context
-
-        sup = super(PersonAssignedBugTaskSearchListingView, self)
-        return sup.searchUnbatched(
-            searchtext, context, extra_params, prejoins)
-
-    def shouldShowAssigneeWidget(self):
-        """Should the assignee widget be shown on the advanced search page?"""
-        return False
-
-    def shouldShowTeamPortlet(self):
-        """Should the team assigned bugs portlet be shown?"""
-        return True
-
-    def shouldShowTagsCombinatorWidget(self):
-        """Should the tags combinator widget show on the search page?"""
-        return False
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "assigned to %s" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        """The header for the search page."""
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        """The Search button for the advanced search page."""
-        return "Search bugs %s" % self.context_description
-
-    def getSimpleSearchURL(self):
-        """Return a URL that can be used as an href to the simple search."""
-        return canonical_url(self.context, view_name="+assignedbugs")
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonCommentedBugTaskSearchListingView(RelevantMilestonesMixin,
-                                              BugTaskSearchListingView):
-    """All bugs commented on by a Person."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    page_title = 'Commented bugs'
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the open bugs commented on by a person."""
-        if context is None:
-            context = self.context
-
-        if extra_params is None:
-            extra_params = dict()
-        else:
-            extra_params = dict(extra_params)
-        extra_params['bug_commenter'] = context
-
-        sup = super(PersonCommentedBugTaskSearchListingView, self)
-        return sup.searchUnbatched(
-            searchtext, context, extra_params, prejoins)
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "commented on by %s" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        """The header for the search page."""
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        """The Search button for the advanced search page."""
-        return "Search bugs %s" % self.context_description
-
-    def getSimpleSearchURL(self):
-        """Return a URL that can be used as an href to the simple search."""
-        return canonical_url(self.context, view_name="+commentedbugs")
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonReportedBugTaskSearchListingView(RelevantMilestonesMixin,
-                                             BugTaskSearchListingView):
-    """All bugs reported by someone."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    page_title = 'Reported bugs'
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the bugs reported by a person."""
-        if context is None:
-            context = self.context
-
-        if extra_params is None:
-            extra_params = dict()
-        else:
-            extra_params = dict(extra_params)
-        # Specify both owner and bug_reporter to try to prevent the same
-        # bug (but different tasks) being displayed.
-        extra_params['owner'] = context
-        extra_params['bug_reporter'] = context
-
-        sup = super(PersonReportedBugTaskSearchListingView, self)
-        return sup.searchUnbatched(
-            searchtext, context, extra_params, prejoins)
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "reported by %s" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        """The header for the search page."""
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        """The Search button for the advanced search page."""
-        return "Search bugs %s" % self.context_description
-
-    def getSimpleSearchURL(self):
-        """Return a URL that can be used as an href to the simple search."""
-        return canonical_url(self.context, view_name="+reportedbugs")
-
-    def shouldShowReporterWidget(self):
-        """Should the reporter widget be shown on the advanced search page?"""
-        return False
-
-    def shouldShowTagsCombinatorWidget(self):
-        """Should the tags combinator widget show on the search page?"""
-        return False
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonSubscribedBugTaskSearchListingView(RelevantMilestonesMixin,
-                                               BugTaskSearchListingView):
-    """All bugs someone is subscribed to."""
-
-    columns_to_show = ["id", "summary", "bugtargetdisplayname",
-                       "importance", "status"]
-    page_title = 'Subscribed bugs'
-    view_name = '+subscribedbugs'
-
-    def searchUnbatched(self, searchtext=None, context=None,
-                        extra_params=None, prejoins=[]):
-        """Return the bugs subscribed to by a person."""
-        if context is None:
-            context = self.context
-
-        if extra_params is None:
-            extra_params = dict()
-        else:
-            extra_params = dict(extra_params)
-        extra_params['subscriber'] = context
-
-        sup = super(PersonSubscribedBugTaskSearchListingView, self)
-        return sup.searchUnbatched(
-            searchtext, context, extra_params, prejoins)
-
-    def shouldShowTeamPortlet(self):
-        """Should the team subscribed bugs portlet be shown?"""
-        return True
-
-    @property
-    def context_description(self):
-        """See `BugTaskSearchListingView`."""
-        return "%s is subscribed to" % self.context.displayname
-
-    def getSearchPageHeading(self):
-        """The header for the search page."""
-        return "Bugs %s" % self.context_description
-
-    def getAdvancedSearchButtonLabel(self):
-        """The Search button for the advanced search page."""
-        return "Search bugs %s is Cc'd to" % self.context.displayname
-
-    def getSimpleSearchURL(self):
-        """Return a URL that can be used as an href to the simple search."""
-        return canonical_url(self.context, view_name="+subscribedbugs")
-
-    @property
-    def label(self):
-        return self.getSearchPageHeading()
-
-
-class PersonSubscriptionsView(LaunchpadView):
-    """All the subscriptions for a person."""
-
-    page_title = 'Subscriptions'
-
-    def subscribedBugTasks(self):
-        """
-        Return a BatchNavigator for distinct bug tasks to which the person is
-        subscribed.
-        """
-        bug_tasks = self.context.searchTasks(None, user=self.user,
-            order_by='-date_last_updated',
-            status=(BugTaskStatus.NEW,
-                    BugTaskStatus.INCOMPLETE,
-                    BugTaskStatus.CONFIRMED,
-                    BugTaskStatus.TRIAGED,
-                    BugTaskStatus.INPROGRESS,
-                    BugTaskStatus.FIXCOMMITTED,
-                    BugTaskStatus.INVALID),
-            bug_subscriber=self.context)
-
-        sub_bug_tasks = []
-        sub_bugs = set()
-
-        # XXX: GavinPanella 2010-10-08 bug=656904: This materializes the
-        # entire result set. It would probably be more efficient implemented
-        # with a pre_iter_hook on a DecoratedResultSet.
-        for task in bug_tasks:
-            # We order the bugtasks by date_last_updated but we always display
-            # the default task for the bug. This is to avoid ordering issues
-            # in tests and also prevents user confusion (because nothing is
-            # more confusing than your subscription targets changing seemingly
-            # at random).
-            if task.bug not in sub_bugs:
-                # XXX: GavinPanella 2010-10-08 bug=656904: default_bugtask
-                # causes a query to be executed. It would be more efficient to
-                # get the default bugtask in bulk, in a pre_iter_hook on a
-                # DecoratedResultSet perhaps.
-                sub_bug_tasks.append(task.bug.default_bugtask)
-                sub_bugs.add(task.bug)
-
-        return BatchNavigator(sub_bug_tasks, self.request)
-
-    def canUnsubscribeFromBugTasks(self):
-        """Can the current user unsubscribe from the bug tasks shown?"""
-        return (self.user is not None and
-                self.user.inTeam(self.context))
-
-    @property
-    def label(self):
-        """The header for the subscriptions page."""
-        return "Subscriptions for %s" % self.context.displayname
-
-
-class PersonStructuralSubscriptionsView(LaunchpadView):
-    """All the structural subscriptions for a person."""
-
-    page_title = 'Structural subscriptions'
-
-    def canUnsubscribeFromBugTasks(self):
-        """Can the current user modify subscriptions for the context?"""
-        return (self.user is not None and
-                self.user.inTeam(self.context))
-
-    @property
-    def label(self):
-        """The header for the structural subscriptions page."""
-        return "Structural subscriptions for %s" % self.context.displayname
-
-
 class PersonVouchersView(LaunchpadFormView):
     """Form for displaying and redeeming commercial subscription vouchers."""
 

=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py	2012-02-16 03:12:17 +0000
+++ lib/lp/registry/browser/team.py	2012-02-18 01:30:26 +0000
@@ -1752,6 +1752,8 @@
 class TeamMembershipView(LaunchpadView):
     """The view behind ITeam/+members."""
 
+    page_title = 'Members'
+
     @cachedproperty
     def label(self):
         return smartquote('Members of "%s"' % self.context.displayname)

=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py	2012-02-16 03:12:17 +0000
+++ lib/lp/registry/browser/tests/test_team.py	2012-02-18 01:30:26 +0000
@@ -535,7 +535,7 @@
             }
         with person_logged_in(team.teamowner):
             with FeatureFixture(self.feature_flag):
-                view = create_initialized_view(
+                create_initialized_view(
                     personset, name=self.view_name, principal=team.teamowner,
                     form=form)
             team = personset.getByName(team_name)
@@ -571,7 +571,7 @@
             self.assertEqual(
                 ['PRIVATE'],
                 browser.getControl(name="field.visibility").value)
-        
+
 
 class TestTeamMenu(TestCaseWithFactory):
 
@@ -778,6 +778,17 @@
             view.add_action.success(data={'newmember': member_team})
 
 
+class TeamMembershipViewTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_init(self):
+        team = self.factory.makeTeam(name='pting')
+        view = create_initialized_view(team, name='+members')
+        self.assertEqual('Members', view.page_title)
+        self.assertEqual(u'Members of \u201cPting\u201d', view.label)
+
+
 class TestTeamIndexView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-02-16 08:27:37 +0000
+++ lib/lp/registry/model/person.py	2012-02-18 01:30:26 +0000
@@ -40,8 +40,13 @@
 import weakref
 
 from lazr.delegates import delegates
-from lazr.restful.utils import get_current_browser_request
+from lazr.restful.utils import (
+    get_current_browser_request,
+    smartquote,
+    )
+
 import pytz
+
 from sqlobject import (
     BoolCol,
     ForeignKey,
@@ -1786,6 +1791,8 @@
     @property
     def title(self):
         """See `IPerson`."""
+        if self.is_team:
+            return smartquote('"%s" team') % self.displayname
         return self.displayname
 
     @property

=== modified file 'lib/lp/registry/stories/person/xx-admin-person-review.txt'
--- lib/lp/registry/stories/person/xx-admin-person-review.txt	2012-01-15 13:32:27 +0000
+++ lib/lp/registry/stories/person/xx-admin-person-review.txt	2012-02-18 01:30:26 +0000
@@ -25,12 +25,8 @@
 
 The review page has a link to the review account page.
 
-    >>> admin_browser.getLink('Administer').click()
-    >>> link = admin_browser.getLink(url='+reviewaccount')
-    >>> print link.text
-    edit[IMG] Review the user's account information
-
-    >>> link.click()
+    >>> admin_browser.open('http://launchpad.dev/~salgado')
+    >>> admin_browser.getLink('Administer Account').click()
     >>> print admin_browser.title
     Review person's account...
 
@@ -51,45 +47,6 @@
     >>> print link.text
     edit[IMG] Review the user's Launchpad information
 
-The admin can update the user's account. Suspending an account will give a
-feedback message.
-
-    >>> admin_browser.getControl(
-    ...     'The status of this account').value = ['SUSPENDED']
-    >>> admin_browser.getControl(
-    ...     name='field.status_comment').value = 'Bad boy.'
-    >>> admin_browser.getControl('Change').click()
-
-    >>> print admin_browser.title
-    The one and only Salgado does not use Launchpad
-    >>> print get_feedback_messages(admin_browser.contents)[0]
-    The account "Guilherme Salgado" has been suspended.
-
-The admin can see the account information of a user that does not use
-Launchpad, and can change the account too. Note that all pages that belong
-to a suspended user have a 410 status code to ensure search engines remove
-them from their index.
-
-    >>> admin_browser.getLink('Administer').click()
-    >>> admin_browser.getLink(url='+reviewaccount').click()
-    >>> print admin_browser.title
-    Review person's account...
-
-    >>> control = admin_browser.getControl(name='field.status_comment')
-    >>> print control.value
-    Bad boy.
-
-    >>> control.value = 'Reinstated after he apologised'
-    >>> admin_browser.getControl(
-    ...     'The status of this account').value = ['ACTIVE']
-    >>> admin_browser.getControl('Change').click()
-    >>> print admin_browser.title
-    The one and only Salgado does not use Launchpad
-
-    >>> for message in get_feedback_messages(admin_browser.contents):
-    ...     print message
-    The user is reactivated. He must use the "forgot password" to log in.
-
 
 Registry experts
 ----------------

=== modified file 'lib/lp/registry/templates/person-review.pt'
--- lib/lp/registry/templates/person-review.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/person-review.pt	2012-02-18 01:30:26 +0000
@@ -25,12 +25,6 @@
               may cause problems with relying parties. PPA and mailing lists
               will be broken too.
             </p>
-            <p>
-              <a tal:attributes="
-                href string:${view/context/fmt:url}/+reviewaccount"><img
-                tal:attributes="alt string:edit" src="/@@/edit" />
-                Review the user's account information</a>.
-            </p>
           </tal:review-person>
 
           <tal:review-account

=== modified file 'lib/lp/registry/templates/team-members.pt'
--- lib/lp/registry/templates/team-members.pt	2010-10-19 19:42:44 +0000
+++ lib/lp/registry/templates/team-members.pt	2012-02-18 01:30:26 +0000
@@ -13,8 +13,6 @@
      tal:define="user_can_edit_memberships context/required:launchpad.Edit;
                  active_member_count context/active_member_count">
 
-  <p>Active, pending and former members of this team.</p>
-
   <ul>
     <li tal:condition="active_member_count"
         tal:define="membership_batch nocall:view/active_memberships/currentBatch">

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2012-02-16 08:27:37 +0000
+++ lib/lp/registry/tests/test_person.py	2012-02-18 01:30:26 +0000
@@ -6,7 +6,10 @@
 from datetime import datetime
 
 from lazr.lifecycle.snapshot import Snapshot
+from lazr.restful.utils import smartquote
+
 import pytz
+
 from storm.store import Store
 from testtools.matchers import (
     Equals,
@@ -261,6 +264,16 @@
 
     layer = DatabaseFunctionalLayer
 
+    def test_title_user(self):
+        user = self.factory.makePerson(name='snarf')
+        self.assertEqual('Snarf', user.title)
+        self.assertEqual(user.displayname, user.title)
+
+    def test_title_team(self):
+        team = self.factory.makeTeam(name='pting')
+        title = smartquote('"%s" team') % team.displayname
+        self.assertEqual(title, team.title)
+
     def test_getOwnedOrDrivenPillars(self):
         user = self.factory.makePerson()
         active_project = self.factory.makeProject(owner=user)

=== modified file 'lib/lp/services/webapp/error.py'
--- lib/lp/services/webapp/error.py	2012-01-23 21:23:24 +0000
+++ lib/lp/services/webapp/error.py	2012-02-18 01:30:26 +0000
@@ -41,7 +41,6 @@
     implements(ISystemErrorView)
 
     page_title = 'Error: Launchpad system error'
-    override_title_breadcrumbs = True
 
     plain_oops_template = ViewPageTemplateFile(
         'templates/oops-veryplain.pt')
@@ -185,7 +184,6 @@
 class NotFoundView(SystemErrorView):
 
     page_title = 'Error: Page not found'
-    override_title_breadcrumbs = True
 
     response_code = httplib.NOT_FOUND
 
@@ -224,7 +222,6 @@
 class RequestExpiredView(SystemErrorView):
 
     page_title = 'Error: Timeout'
-    override_title_breadcrumbs = True
 
     response_code = httplib.SERVICE_UNAVAILABLE
 
@@ -240,7 +237,6 @@
     """View rendered when an InvalidBatchSizeError is raised."""
 
     page_title = "Error: Invalid Batch Size"
-    override_title_breadcrumbs = True
 
     response_code = httplib.BAD_REQUEST
 
@@ -259,7 +255,6 @@
 class TranslationUnavailableView(SystemErrorView):
 
     page_title = 'Error: Translation page is not available'
-    override_title_breadcrumbs = True
 
     response_code = httplib.SERVICE_UNAVAILABLE
 
@@ -271,7 +266,6 @@
     """View rendered when an InvalidBatchSizeError is raised."""
 
     page_title = "Error: you can't do this right now"
-    override_title_breadcrumbs = True
 
     response_code = httplib.SERVICE_UNAVAILABLE
 


Follow ups