launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02280
[Merge] lp:~allenap/launchpad/sub-search-ui-bug-656823-3 into lp:launchpad
Gavin Panella has proposed merging lp:~allenap/launchpad/sub-search-ui-bug-656823-3 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~allenap/launchpad/sub-search-ui-bug-656823-3/+merge/45349
Adds a view to display bug subscription filters (of which a structural
subscription can have zero or more). It uses this view in
Person:+structural-subscriptions.
Note: Editing bug filters is only currently available via the web
service API, and the +structural-subscriptions page, while available
publicly, is not linked to from anywhere yet.
To try this out you'll need to create a bug subscription filter. In
bin/harness:
{{{
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProductSet
from lp.bugs.interfaces.bugtask import BugTaskStatus, BugTaskImportance
name16 = getUtility(IPersonSet).getByName("name16")
firefox = getUtility(IProductSet).getByName("firefox")
subscription = firefox.addBugSubscription(name16, name16)
bug_filter = subscription.newBugFilter()
transaction.commit()
}}}
Visit https://launchpad.dev/~name16/+structural-subscriptions.
You can customize the bug filter some more:
{{{
bug_filter.description = u"Bug Jam 2010"
bug_filter.tags = [u"bugjam2010"]
bug_filter.statuses = [BugTaskStatus.NEW]
transaction.commit()
}}}
There's also .find_all_tags (boolean) and .importances attributes to
customize.
--
https://code.launchpad.net/~allenap/launchpad/sub-search-ui-bug-656823-3/+merge/45349
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/sub-search-ui-bug-656823-3 into lp:launchpad.
=== added file 'lib/lp/bugs/browser/bugsubscriptionfilter.py'
--- lib/lp/bugs/browser/bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/bugsubscriptionfilter.py 2011-01-06 11:37:06 +0000
@@ -0,0 +1,68 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""View classes for bug subscription filters."""
+
+__metaclass__ = type
+__all__ = [
+ "BugSubscriptionFilterView",
+ ]
+
+
+from functools import wraps
+
+from canonical.launchpad.helpers import english_list
+from canonical.launchpad.webapp.publisher import LaunchpadView
+
+
+def listify(func):
+ """Decorator to listify the return value of the wrapped function."""
+
+ @wraps(func)
+ def wrapper(self):
+ return list(func(self))
+ return wrapper
+
+
+class BugSubscriptionFilterView(LaunchpadView):
+ """View for `IBugSubscriptionFilter`.
+
+ The properties and methods herein are intended to construct a view
+ something like:
+
+ Bug mail filter "A filter description"
+ Matches when status is New, Confirmed, or Triaged
+ *and* importance is High, Medium, or Low
+ *and* the bug is tagged with bar or foo.
+
+ """
+
+ @property
+ def description(self):
+ """Return the bug filter description.
+
+ Leading and trailing whitespace is trimmed. If the description is not
+ set the empty string is returned.
+ """
+ description = self.context.description
+ return u"" if description is None else description.strip()
+
+ @property
+ @listify
+ def conditions(self):
+ """Descriptions of the bug filter's conditions."""
+ statuses = self.context.statuses
+ if len(statuses) > 0:
+ yield u"the status is %s" % english_list(
+ (status.title for status in sorted(statuses)),
+ conjunction=u"or")
+ importances = self.context.importances
+ if len(importances) > 0:
+ yield u"the importance is %s" % english_list(
+ (importance.title for importance in sorted(importances)),
+ conjunction=u"or")
+ tags = self.context.tags
+ if len(tags) > 0:
+ yield u"the bug is tagged with %s" % english_list(
+ sorted(tags), conjunction=(
+ u"and" if self.context.find_all_tags else u"or"))
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2010-11-24 14:53:31 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-01-06 11:37:06 +0000
@@ -1183,6 +1183,12 @@
path_expression="string:+filter/${id}"
attribute_to_parent="structural_subscription"
rootsite="bugs" />
+ <browser:page
+ for="lp.bugs.interfaces.bugsubscriptionfilter.IBugSubscriptionFilter"
+ class=".bugsubscriptionfilter.BugSubscriptionFilterView"
+ template="../templates/bug-subscription-filter.pt"
+ permission="launchpad.View"
+ name="+definition" />
</facet>
</configure>
=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
--- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2010-12-21 15:49:12 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-01-06 11:37:06 +0000
@@ -9,6 +9,7 @@
from urlparse import urlparse
from lazr.restfulclient.errors import BadRequest
+from lxml import html
from storm.exceptions import LostObjectError
from testtools.matchers import StartsWith
import transaction
@@ -27,10 +28,12 @@
StructuralSubscriptionNavigation,
)
from lp.testing import (
+ normalize_whitespace,
person_logged_in,
TestCaseWithFactory,
ws_object,
)
+from lp.testing.views import create_initialized_view
class TestBugSubscriptionFilterBase:
@@ -208,3 +211,120 @@
self.assertRaises(
LostObjectError, getattr, self.subscription_filter,
"find_all_tags")
+
+
+class TestBugSubscriptionFilterView(
+ TestBugSubscriptionFilterBase, TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestBugSubscriptionFilterView, self).setUp()
+ self.view = create_initialized_view(
+ self.subscription_filter, "+definition")
+
+ def test_description(self):
+ # If the description is not set then the empty string is returned.
+ self.assertEqual(u"", self.view.description)
+ # If the description is just whitespace then the empty string is
+ # returned.
+ with person_logged_in(self.owner):
+ self.subscription_filter.description = u" "
+ self.assertEqual(u"", self.view.description)
+ # If the description is set it is returned.
+ with person_logged_in(self.owner):
+ self.subscription_filter.description = u"Foo"
+ self.assertEqual(u"Foo", self.view.description)
+ # Leading and trailing whitespace is trimmed.
+ with person_logged_in(self.owner):
+ self.subscription_filter.description = u" Foo\t "
+ self.assertEqual(u"Foo", self.view.description)
+
+ def test_conditions_with_nothing_set(self):
+ # If nothing is set the conditions list is empty.
+ self.assertEqual([], self.view.conditions)
+
+ def test_conditions_for_statuses(self):
+ # If no statuses have been specified nothing is returned.
+ self.assertEqual([], self.view.conditions)
+ # If set, a description of the statuses is returned.
+ with person_logged_in(self.owner):
+ self.subscription_filter.statuses = [
+ BugTaskStatus.NEW,
+ BugTaskStatus.CONFIRMED,
+ BugTaskStatus.TRIAGED,
+ ]
+ self.assertEqual(
+ [u"the status is New, Confirmed, or Triaged"],
+ self.view.conditions)
+
+ def test_conditions_for_importances(self):
+ # If no importances have been specified nothing is returned.
+ self.assertEqual([], self.view.conditions)
+ # If set, a description of the importances is returned.
+ with person_logged_in(self.owner):
+ self.subscription_filter.importances = [
+ BugTaskImportance.LOW,
+ BugTaskImportance.MEDIUM,
+ BugTaskImportance.HIGH,
+ ]
+ self.assertEqual(
+ [u"the importance is High, Medium, or Low"],
+ self.view.conditions)
+
+ def test_conditions_for_tags(self):
+ # If no tags have been specified nothing is returned.
+ self.assertEqual([], self.view.conditions)
+ # If set, a description of the tags is returned.
+ with person_logged_in(self.owner):
+ self.subscription_filter.tags = [u"foo", u"bar", u"*"]
+ self.assertEqual(
+ [u"the bug is tagged with *, bar, or foo"],
+ self.view.conditions)
+ # If find_all_tags is set, the conjunction changes.
+ with person_logged_in(self.owner):
+ self.subscription_filter.find_all_tags = True
+ self.assertEqual(
+ [u"the bug is tagged with *, bar, and foo"],
+ self.view.conditions)
+
+ def assertRender(self, dt_content=None, dd_content=None):
+ root = html.fromstring(self.view.render())
+ if dt_content is not None:
+ self.assertEqual(
+ dt_content, normalize_whitespace(
+ root.find("dt").text_content()))
+ if dd_content is not None:
+ self.assertEqual(
+ dd_content, normalize_whitespace(
+ root.find("dd").text_content()))
+
+ def test_render_with_nothing_set(self):
+ # If no conditions are set, the rendered description is very simple.
+ self.assertRender(u"Bug mail filter", u"Matches nothing")
+
+ def test_render_with_description(self):
+ # If a description is set it appears in the content of the dt tag,
+ # surrounded by "curly" quotes.
+ with person_logged_in(self.owner):
+ self.subscription_filter.description = u"The Wait"
+ self.assertRender(u"Bug mail filter \u201cThe Wait\u201d")
+
+ def test_render_with_conditions(self):
+ # If conditions are set, a human-comprehensible message is composed by
+ # joining the conditions with "and".
+ with person_logged_in(self.owner):
+ self.subscription_filter.statuses = [
+ BugTaskStatus.NEW,
+ BugTaskStatus.CONFIRMED,
+ BugTaskStatus.TRIAGED,
+ ]
+ self.subscription_filter.importances = [
+ BugTaskImportance.LOW,
+ BugTaskImportance.MEDIUM,
+ BugTaskImportance.HIGH,
+ ]
+ self.subscription_filter.tags = [u"foo", u"bar"]
+ self.assertRender(
+ dd_content=(
+ u"Matches when " + u" and ".join(self.view.conditions)))
=== added file 'lib/lp/bugs/templates/bug-subscription-filter-edit.pt'
--- lib/lp/bugs/templates/bug-subscription-filter-edit.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bug-subscription-filter-edit.pt 2011-01-06 11:37:06 +0000
@@ -0,0 +1,13 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ xml:lang="en" lang="en" dir="ltr"
+ metal:use-macro="view/macro:page/main_only"
+ i18n:domain="malone">
+ <body>
+ <div metal:fill-slot="main">
+ </div>
+ </body>
+</html>
=== added file 'lib/lp/bugs/templates/bug-subscription-filter.pt'
--- lib/lp/bugs/templates/bug-subscription-filter.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bug-subscription-filter.pt 2011-01-06 11:37:06 +0000
@@ -0,0 +1,27 @@
+<dl
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ xml:lang="en" lang="en" dir="ltr"
+ tal:define="conditions view/conditions"
+ tal:omit-tag="">
+ <dt tal:define="description view/description">
+ Bug mail filter
+ <em tal:condition="description">
+ “<span tal:content="view/description" />”
+ </em>
+ </dt>
+ <dd tal:condition="conditions">
+ Matches when
+ <tal:conditions repeat="condition conditions">
+ <span tal:content="condition" />
+ <tal:conjunction condition="not:repeat/condition/end">
+ <br />and
+ </tal:conjunction>
+ </tal:conditions>
+ </dd>
+ <dd tal:condition="not:conditions">
+ <em>Matches nothing</em>
+ </dd>
+</dl>
=== modified file 'lib/lp/registry/stories/person/xx-person-subscriptions.txt'
--- lib/lp/registry/stories/person/xx-person-subscriptions.txt 2010-11-05 12:04:07 +0000
+++ lib/lp/registry/stories/person/xx-person-subscriptions.txt 2011-01-06 11:37:06 +0000
@@ -62,7 +62,8 @@
... auth='Basic %s:g00dpassword' %
... subscriber.preferredemail.email)
>>> logout()
- >>> subscriber_browser.open('http://bugs.launchpad.dev/~webster/+subscriptions')
+ >>> subscriber_browser.open(
+ ... 'http://bugs.launchpad.dev/~webster/+subscriptions')
>>> unsub_link = subscriber_browser.getLink(
... id='unsubscribe-subscriber-%s' % subscriber.id)
>>> unsub_link.click()
@@ -84,7 +85,8 @@
>>> subscriber_browser.getLink(url='/affluenza/+bug/%s/+subscribe'
... % bugB.id).click()
- >>> subscriber_browser.getControl("Unsubscribe me from this bug").selected = True
+ >>> subscriber_browser.getControl(
+ ... "Unsubscribe me from this bug").selected = True
>>> subscriber_browser.getControl("Continue").click()
>>> print subscriber_browser.title
Subscriptions : Webster
@@ -92,7 +94,8 @@
Webster can see that the bug about Affluenza is no longer listed in his direct
bug subscriptions.
- >>> subscriber_browser.open('http://bugs.launchpad.dev/~webster/+subscriptions')
+ >>> subscriber_browser.open(
+ ... 'http://bugs.launchpad.dev/~webster/+subscriptions')
>>> print extract_text(find_tag_by_id(
... subscriber_browser.contents, 'bug_subscriptions'))
Summary
@@ -114,7 +117,8 @@
Webster chooses to review the subscriptions for his team America.
- >>> subscriber_browser.open('https://bugs.launchpad.dev/~america/+subscriptions')
+ >>> subscriber_browser.open(
+ ... 'https://bugs.launchpad.dev/~america/+subscriptions')
>>> print subscriber_browser.title
Subscriptions : âAmericaâ team
@@ -139,7 +143,8 @@
... 'Unsubscribe America from this bug').selected = True
>>> subscriber_browser.getControl('Continue').click()
- >>> subscriber_browser.open('https://launchpad.dev/~america/+subscriptions')
+ >>> subscriber_browser.open(
+ ... 'https://launchpad.dev/~america/+subscriptions')
>>> page_text = extract_text(
... find_main_content(subscriber_browser.contents))
>>> "does not have any direct bug subscriptions" in page_text
@@ -203,3 +208,37 @@
>>> print extract_text(find_tag_by_id(
... subscriber_browser.contents, "structural-subscriptions"))
Webster does not have any structural subscriptions.
+
+
+Subscriptions with Bug Filters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If bug mail filters exist for a structural subscription they are
+displayed immediately after the subscription.
+
+ >>> from lp.testing import celebrity_logged_in, person_logged_in
+
+ >>> with celebrity_logged_in("admin"):
+ ... nigel = factory.makePerson(
+ ... name="nigel", password="g00dpassword")
+ >>> with person_logged_in(nigel):
+ ... nigel_subscription = scofflaw.addBugSubscription(nigel, nigel)
+ ... nigel_bug_filter1 = nigel_subscription.newBugFilter()
+ ... nigel_bug_filter1.description = u"First"
+ ... nigel_bug_filter1.tags = [u"foo"]
+ ... nigel_bug_filter2 = nigel_subscription.newBugFilter()
+ ... nigel_bug_filter2.description = u"Second"
+ ... nigel_bug_filter2.tags = [u"bar"]
+ ... nigel_browser = setupBrowser(
+ ... auth='Basic %s:g00dpassword' % nigel.preferredemail.email)
+
+ >>> nigel_browser.open(
+ ... "http://launchpad.dev/people/+me/+structural-subscriptions")
+ >>> subscriptions = find_tag_by_id(
+ ... nigel_browser.contents, 'structural-subscriptions')
+ >>> for subscription in subscriptions.findAll("li"):
+ ... print extract_text(subscription.dl)
+ Bug mail filter “First”
+ Matches when the bug is tagged with foo
+ Bug mail filter “Second”
+ Matches when the bug is tagged with bar
=== modified file 'lib/lp/registry/templates/person-structural-subscriptions.pt'
--- lib/lp/registry/templates/person-structural-subscriptions.pt 2010-10-11 14:34:27 +0000
+++ lib/lp/registry/templates/person-structural-subscriptions.pt 2011-01-06 11:37:06 +0000
@@ -31,6 +31,11 @@
title string:Modify subscription to ${subscription/target/title};">
<img src="/@@/edit" />
</a>
+ <dl style="padding-left: 2em">
+ <tal:bug-filters
+ repeat="bug_filter subscription/bug_filters"
+ content="structure bug_filter/@@+definition" />
+ </dl>
</li>
</ul>
Follow ups