launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06414
[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