← Back to team overview

launchpad-reviewers team mailing list archive

[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">
+      &#8220;<span tal:content="view/description" />&#8221;
+    </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 &#8220;First&#8221;
+    Matches when the bug is tagged with foo
+    Bug mail filter &#8220;Second&#8221;
+    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