Merge lp:~michael.nelson/launchpad/644328-blacklist-ui into lp:launchpad/db-devel

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: 9843
Proposed branch: lp:~michael.nelson/launchpad/644328-blacklist-ui
Merge into: lp:launchpad/db-devel
Diff against target: 774 lines (+414/-67)
15 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+1/-1)
lib/canonical/launchpad/webapp/interfaces.py (+4/-0)
lib/canonical/launchpad/webapp/servers.py (+23/-19)
lib/canonical/launchpad/webapp/tests/test_servers.py (+14/-0)
lib/lp/registry/browser/distroseriesdifference.py (+50/-4)
lib/lp/registry/browser/tests/test_distroseriesdifference_views.py (+79/-3)
lib/lp/registry/browser/tests/test_distroseriesdifference_webservice.py (+25/-0)
lib/lp/registry/browser/tests/test_series_views.py (+0/-9)
lib/lp/registry/interfaces/distroseriesdifference.py (+11/-0)
lib/lp/registry/javascript/distroseriesdifferences_details.js (+122/-28)
lib/lp/registry/model/distroseriesdifference.py (+5/-0)
lib/lp/registry/templates/distroseries-localdifferences.pt (+4/-2)
lib/lp/registry/templates/distroseriesdifference-listing-extra.pt (+12/-0)
lib/lp/registry/tests/test_distroseriesdifference.py (+33/-0)
lib/lp/registry/windmill/tests/test_distroseriesdifference_expander.py (+31/-1)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/644328-blacklist-ui
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Curtis Hovey (community) ui Approve
Guilherme Salgado (community) ui* Approve
Review via email: mp+36442@code.launchpad.net

Commit message

Adds UI for blacklisting distroseriesdifferences.

Description of the change

Overview
========

This branch implements the UI for blacklisting source package differences for derived distro series.

See the LEP here:
https://dev.launchpad.net/LEP/DerivativeDistributions

and the specific mockup here:
https://dev.launchpad.net/LEP/DerivativeDistributions?action=AttachFile&do=get&target=derived-series-diffs_uiround2.png

You can view a 1min demo of the UI here:

http://people.canonical.com/~michaeln/tmp/644328-blacklist-ui.ogv

updated after salgado's ui review to:
http://people.canonical.com/~michaeln/tmp/644328-review-salgado.ogv

(Note: the issue I mention about the default radio not being selected in chromium was resolved by wrapping the inputs in an empty form element).

To test
=======
bin/test -vv -m test_servers -m test_distroseriesdifference_views -m test_distroseriesdifference_webservice -m test_distroseriesdifference_expander

To demo locally:
================
Run http://pastebin.ubuntu.com/494656/ in bin/iharness and then open
https://launchpad.dev/ubuntu/hoary/+localpackagediffs

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :

I forgot to mention, I've ventured slightly from that mockup after some earlier discussions with Henning... that's why 'Add to blacklist' on the mockup has been replaced by two separate links.

Revision history for this message
Guilherme Salgado (salgado) wrote :

<salgado> noodles775, I like the graying out because it makes it easy to see everything that was changed at a glance and maybe undo, but the text (as well as the links) is a bit too verbose (IMHO) and don't allow switching between all different states, so I was thinking of using 3 radio buttons there. "Blacklisted: (*) No, ( ) This version, ( ) All versions"
<salgado> but that's more like a nice-to-have thing, not that important
<noodles775> salgado: that actually sounds a bit like what I'd proposed on the mockup (a single 'Add to blacklist' that would then give you those options), but henninge convinced me on a previous MP that it was a bit too much... hangon, I'll find the discussion.
<salgado> well, except that the radio buttons allow you to see all the possible states without having to click anything
<salgado> just like the links do
<salgado> but sure, maybe he'll be able to convince me as well. :)
<noodles775> salgado: ok - if you search for blacklist at https://code.edge.launchpad.net/~michael.nelson/launchpad/distro-series-difference-browser2/+merge/34739 you'll see a bit of background.
<noodles775> salgado: but I think your radio button option you mention could work too... Also, if you've got suggestions for making the text less verbose while still being easy to understand, let me know :)
<noodles775> s/you/that you/
<salgado> what I like about the radio button is that you see all the options without having to click anything and there's no repetition
<salgado> the downside is that you need vertical real estate, but you seem to have plenty of that
<salgado> as for the text of the link, the only thing I can think of is something like "Blacklist: _this version_ or _all versions_"
<salgado> but I don't quite like that as it seems to suggest you need to blacklist something
<noodles775> salgado: Yeah. RE the radio buttons, I was going to say that it would require two clicks (ie select/submit), but it doesn't of course... we could do the api update when an option is selected quite easily. And that would mean no need for 'Undo'. I like it more and more :)
<salgado> indeed, I hadn't thought about the undo

Revision history for this message
Curtis Hovey (sinzui) wrote :

I radio button the suggestion since it provides context. If I blacklist all version, I can still see an option to black list one version (and know I have made a mistake). This assumes there are three buttons and each fires an ajax event on change.

I think we have a "Undo" problem in Lp. It needs a standard presentation, an icon and rules about where is goes (like "or _Cancel_") so that users know what to expect. As Henning remarked in the other review, the text of the action will vary in size and that could make the list of several open rows hard to scan.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hi Guilherme,

Here's an updated screencast based on your UI review:
http://people.canonical.com/~michaeln/tmp/644328-review-salgado.ogv

Let me know if it's what you were thinking.

Revision history for this message
Guilherme Salgado (salgado) wrote :

That works really nice, thanks for doing it, Michael. It'd be important to try and make the default radio button be initially selected on chromium as well, but if it turns out to be too tricky, maybe you can just file a bug for now.

Revision history for this message
Guilherme Salgado (salgado) :
review: Approve (ui*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

This looks great. Thanks Michael and Salgado for doing such a good job on this.

review: Approve (ui)
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (20.5 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Michael,
thank you for this cool feature. I don't see any problems with your branch
just a few suggestions some of which we already discussed in IRC

 review approve code

Cheers,
Henning

> === modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
> === modified file 'lib/canonical/launchpad/webapp/interfaces.py'
> === modified file 'lib/canonical/launchpad/webapp/servers.py'
> --- lib/canonical/launchpad/webapp/servers.py 2010-09-22 15:17:39 +0000
> +++ lib/canonical/launchpad/webapp/servers.py 2010-09-27 15:03:26 +0000
> @@ -526,7 +526,27 @@
> return decoded_qs
>
>
> -class BasicLaunchpadRequest:
> +class LaunchpadBrowserRequestMixin:
> + """A mixin for classes that share some method implementations."""

We already talked about how senseless this comment is ... Can you please
improve it?

> +
> + def getRootURL(self, rootsite):
> + """See IBasicLaunchpadRequest."""
> + if rootsite is not None:
> + assert rootsite in allvhosts.configs, (
> + "rootsite is %s. Must be in %r." % (
> + rootsite, sorted(allvhosts.configs.keys())))
> + root_url = allvhosts.configs[rootsite].rooturl
> + else:
> + root_url = self.getApplicationURL() + '/'
> + return root_url
> +
> + @property
> + def is_ajax(self):
> + """See `IBasicLaunchpadRequest`."""
> + return 'XMLHttpRequest' == self.getHeader('HTTP_X_REQUESTED_WITH')

I was just wondering: How much of a standard is the "X-Requested-With" header?
Is there a fall-back if browser does not send it?

> +
> +
> +class BasicLaunchpadRequest(LaunchpadBrowserRequestMixin):
> """Mixin request class to provide stepstogo."""
>
> implements(IBasicLaunchpadRequest)
> @@ -581,24 +601,8 @@
> return get_query_string_params(self)
>
>
> -class LaunchpadBrowserRequestMixin:
> - """A mixin for classes that share some method implementations."""
> -
> - def getRootURL(self, rootsite):
> - """See IBasicLaunchpadRequest."""
> - if rootsite is not None:
> - assert rootsite in allvhosts.configs, (
> - "rootsite is %s. Must be in %r." % (
> - rootsite, sorted(allvhosts.configs.keys())))
> - root_url = allvhosts.configs[rootsite].rooturl
> - else:
> - root_url = self.getApplicationURL() + '/'
> - return root_url
> -
> -
> class LaunchpadBrowserRequest(BasicLaunchpadRequest, BrowserRequest,
> - NotificationRequest, ErrorReportRequest,
> - LaunchpadBrowserRequestMixin):
> + NotificationRequest, ErrorReportRequest):
> """Integration of launchpad mixin request classes to make an uber
> launchpad request class.
> """
> @@ -1370,7 +1374,7 @@
>
>
> class PublicXMLRPCRequest(BasicLaunchpadRequest, XMLRPCRequest,
> - ErrorReportRequest, LaunchpadBrowserRequestMixin):
> + ErrorReportRequest):
> """Request type for doing public XML-RPC in Launchpad."""
>
> def _createResponse(self):
>
...

review: Approve (code)
Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (11.4 KiB)

On Mon, Sep 27, 2010 at 7:49 PM, Henning Eggers
<email address hidden> wrote:
> Review: Approve code
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Hi Michael,
> thank you for this cool feature. I don't see any problems with your branch
> just a few suggestions some of which we already discussed in IRC

Cheers Henninge, comments below.

>> === modified file 'lib/canonical/launchpad/webapp/servers.py'
>> --- lib/canonical/launchpad/webapp/servers.py 2010-09-22 15:17:39 +0000
>> +++ lib/canonical/launchpad/webapp/servers.py 2010-09-27 15:03:26 +0000
>> @@ -526,7 +526,27 @@
>>      return decoded_qs
>>
>>
>> -class BasicLaunchpadRequest:
>> +class LaunchpadBrowserRequestMixin:
>> +    """A mixin for classes that share some method implementations."""
>
> We already talked about how senseless this comment is ... Can you please
> improve it?

Yep - I didn't write it (just moved it), but have updated it to:

"""Provides methods used for both API and web browser requests."""

>> +    @property
>> +    def is_ajax(self):
>> +        """See `IBasicLaunchpadRequest`."""
>> +        return 'XMLHttpRequest' == self.getHeader('HTTP_X_REQUESTED_WITH')
>
> I was just wondering: How much of a standard is the "X-Requested-With" header?
> Is there a fall-back if browser does not send it?

It is sent by most ajax libraries when sending xhr requests. The above
implementation matches Django's implementation.

>> === modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
>> --- lib/canonical/launchpad/webapp/tests/test_servers.py      2010-08-20 20:31:18 +0000
>> +++ lib/canonical/launchpad/webapp/tests/test_servers.py      2010-09-27 15:03:26 +0000
>> @@ -358,6 +358,16 @@
>>              retried_request.response.getHeader('Vary'),
>>              'Cookie, Authorization')
>>
>> +    def test_is_ajax(self):
>
> Please split this test up:
>       def test_is_not_ajax(self):

Done (test_is_ajax_false).

>>      implements(IConversation)
>> +    schema = IDistroSeriesDifferenceForm
>> +    custom_widget('blacklist_options', RadioWidget)
>> +
>> +    @property
>> +    def initial_values(self):
>> +        """Ensure the correct radio button is checked for blacklisting."""
>> +        if self.context.status in (
>> +            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
>> +            DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
>> +            ):
>
> My feel is that multi-line if statements are to be avoided at all costs. ;)
> They just read badly. You can construct the tuple first and use that in the if
> statement.

OK, that's much better.
>> +    def test_show_blacklist_options(self):
>> +        # Blacklist options are shown if requested by an editor via
>> +        # ajax.
>> +        ds_diff = self.factory.makeDistroSeriesDifference()
>> +
>> +        # Without JS, even editors don't see blacklist options.
>> +        with person_logged_in(ds_diff.owner):
>> +            view = create_initialized_view(
>> +                ds_diff, '+listing-distroseries-extra')
>> +        self.assertFalse(view.show_blacklist_options)
>
> Yes, as you mentioned, please split this test up as well.

Done - it's now 3 tests.

>> +    def test_bl...

1=== modified file 'lib/canonical/launchpad/webapp/servers.py'
2--- lib/canonical/launchpad/webapp/servers.py 2010-09-27 14:00:44 +0000
3+++ lib/canonical/launchpad/webapp/servers.py 2010-09-28 06:46:35 +0000
4@@ -527,7 +527,7 @@
5
6
7 class LaunchpadBrowserRequestMixin:
8- """A mixin for classes that share some method implementations."""
9+ """Provides methods used for both API and web browser requests."""
10
11 def getRootURL(self, rootsite):
12 """See IBasicLaunchpadRequest."""
13
14=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
15--- lib/canonical/launchpad/webapp/tests/test_servers.py 2010-09-24 15:38:27 +0000
16+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2010-09-27 15:33:27 +0000
17@@ -358,14 +358,18 @@
18 retried_request.response.getHeader('Vary'),
19 'Cookie, Authorization')
20
21- def test_is_ajax(self):
22- """True if the HTTP_X_REQUESTED_WITH is set appropriately."""
23+ def test_is_ajax_false(self):
24+ """Normal requests do not define HTTP_X_REQUESTED_WITH."""
25 request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})
26+
27 self.assertFalse(request.is_ajax)
28
29+ def test_is_ajax_true(self):
30+ """Requests with HTTP_X_REQUESTED_WITH set are ajax requests."""
31 request = LaunchpadBrowserRequest(StringIO.StringIO(''), {
32 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
33 })
34+
35 self.assertTrue(request.is_ajax)
36
37
38
39=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
40--- lib/lp/registry/browser/distroseriesdifference.py 2010-09-27 13:46:18 +0000
41+++ lib/lp/registry/browser/distroseriesdifference.py 2010-09-28 06:51:57 +0000
42@@ -53,10 +53,11 @@
43 @property
44 def initial_values(self):
45 """Ensure the correct radio button is checked for blacklisting."""
46- if self.context.status in (
47+ blacklisted_statuses = (
48 DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
49 DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
50- ):
51+ )
52+ if self.context.status in blacklisted_statuses:
53 return dict(blacklist_options=self.context.status)
54
55 return dict(blacklist_options='NONE')
56
57=== modified file 'lib/lp/registry/browser/tests/test_distroseriesdifference_views.py'
58--- lib/lp/registry/browser/tests/test_distroseriesdifference_views.py 2010-09-27 14:31:19 +0000
59+++ lib/lp/registry/browser/tests/test_distroseriesdifference_views.py 2010-09-28 06:56:24 +0000
60@@ -8,7 +8,6 @@
61 __metaclass__ = type
62
63 from BeautifulSoup import BeautifulSoup
64-from zope.app.form.browser.itemswidgets import RadioWidget
65 from zope.component import getUtility
66
67 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
68@@ -127,25 +126,32 @@
69 self.assertIs(None, ds_diff.source_pub)
70 self.assertIs(None, view.binary_summaries)
71
72- def test_show_blacklist_options(self):
73+ def test_show_blacklist_options_non_ajax(self):
74+ # Blacklist options are not shown for non-ajax requests.
75+ ds_diff = self.factory.makeDistroSeriesDifference()
76+
77+ # Without JS, even editors don't see blacklist options.
78+ with person_logged_in(ds_diff.owner):
79+ view = create_initialized_view(
80+ ds_diff, '+listing-distroseries-extra')
81+ self.assertFalse(view.show_blacklist_options)
82+
83+ def test_show_blacklist_options_editor(self):
84 # Blacklist options are shown if requested by an editor via
85 # ajax.
86 ds_diff = self.factory.makeDistroSeriesDifference()
87
88- # Without JS, even editors don't see blacklist options.
89- with person_logged_in(ds_diff.owner):
90- view = create_initialized_view(
91- ds_diff, '+listing-distroseries-extra')
92- self.assertFalse(view.show_blacklist_options)
93-
94- # With a JS request, editors do see blacklist options.
95 request = LaunchpadTestRequest(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
96 with person_logged_in(ds_diff.owner):
97 view = create_initialized_view(
98 ds_diff, '+listing-distroseries-extra', request=request)
99 self.assertTrue(view.show_blacklist_options)
100
101+ def test_show_blacklist_options_non_editor(self):
102 # Even with a JS request, non-editors do not see the options.
103+ ds_diff = self.factory.makeDistroSeriesDifference()
104+
105+ request = LaunchpadTestRequest(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
106 view = create_initialized_view(
107 ds_diff, '+listing-distroseries-extra', request=request)
108 self.assertFalse(view.show_blacklist_options)
109@@ -256,7 +262,7 @@
110 self.assertEqual(
111 1, len(soup.findAll('div', {'class': 'blacklist-options'})))
112
113- def test_blacklist_options_initial_values(self):
114+ def test_blacklist_options_initial_values_NONE(self):
115 ds_diff = self.factory.makeDistroSeriesDifference()
116 view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
117
118@@ -264,6 +270,7 @@
119 # as the default value for the field.
120 self.assertEqual('NONE', view.initial_values.get('blacklist_options'))
121
122+ def test_blacklist_options_initial_values_CURRENT(self):
123 from lp.registry.enum import DistroSeriesDifferenceStatus
124 ds_diff = self.factory.makeDistroSeriesDifference(
125 status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
126@@ -273,6 +280,8 @@
127 DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
128 view.initial_values.get('blacklist_options'))
129
130+ def test_blacklist_options_initial_values_ALWAYS(self):
131+ from lp.registry.enum import DistroSeriesDifferenceStatus
132 ds_diff = self.factory.makeDistroSeriesDifference(
133 status=DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS)
134 view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
135
136=== modified file 'lib/lp/registry/javascript/distroseriesdifferences_details.js'
137--- lib/lp/registry/javascript/distroseriesdifferences_details.js 2010-09-27 10:05:52 +0000
138+++ lib/lp/registry/javascript/distroseriesdifferences_details.js 2010-09-28 06:59:24 +0000
139@@ -42,7 +42,7 @@
140 var config = {
141 on: {
142 success: function(updated_entry, args) {
143- // Let the use know this item is now blacklisted.
144+ // Let the user know this item is now blacklisted.
145 blacklist_options_container.one('img').remove();
146 blacklist_options_container.all(
147 'input').set('disabled', false);
148@@ -167,7 +167,6 @@
149 } else {
150 details_row = next_row
151 }
152-
153 details_row.toggleClass('unseen');
154 };
155

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
2--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-09-21 16:02:23 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-09-28 07:13:54 +0000
4@@ -2419,7 +2419,7 @@
5 background-repeat: no-repeat;
6 background-position:right center;
7 }
8-table#packages_list tr.superseded {
9+table#packages_list tr.superseded, tr.blacklisted {
10 background-color: #eee;
11 }
12 ul.latest-ppa-updates li:nth-child(odd) {
13
14=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
15--- lib/canonical/launchpad/webapp/interfaces.py 2010-09-12 11:43:36 +0000
16+++ lib/canonical/launchpad/webapp/interfaces.py 2010-09-28 07:13:54 +0000
17@@ -335,6 +335,10 @@
18 query_string_params = Attribute(
19 'A dictionary of the query string parameters.')
20
21+ is_ajax = Bool(
22+ title=_('Is ajax'), required=False, readonly=True,
23+ description=_("Indicates whether the request is an XMLHttpRequest."))
24+
25 def getRootURL(rootsite):
26 """Return this request's root URL.
27
28
29=== modified file 'lib/canonical/launchpad/webapp/servers.py'
30--- lib/canonical/launchpad/webapp/servers.py 2010-09-22 15:17:39 +0000
31+++ lib/canonical/launchpad/webapp/servers.py 2010-09-28 07:13:54 +0000
32@@ -526,7 +526,27 @@
33 return decoded_qs
34
35
36-class BasicLaunchpadRequest:
37+class LaunchpadBrowserRequestMixin:
38+ """Provides methods used for both API and web browser requests."""
39+
40+ def getRootURL(self, rootsite):
41+ """See IBasicLaunchpadRequest."""
42+ if rootsite is not None:
43+ assert rootsite in allvhosts.configs, (
44+ "rootsite is %s. Must be in %r." % (
45+ rootsite, sorted(allvhosts.configs.keys())))
46+ root_url = allvhosts.configs[rootsite].rooturl
47+ else:
48+ root_url = self.getApplicationURL() + '/'
49+ return root_url
50+
51+ @property
52+ def is_ajax(self):
53+ """See `IBasicLaunchpadRequest`."""
54+ return 'XMLHttpRequest' == self.getHeader('HTTP_X_REQUESTED_WITH')
55+
56+
57+class BasicLaunchpadRequest(LaunchpadBrowserRequestMixin):
58 """Mixin request class to provide stepstogo."""
59
60 implements(IBasicLaunchpadRequest)
61@@ -581,24 +601,8 @@
62 return get_query_string_params(self)
63
64
65-class LaunchpadBrowserRequestMixin:
66- """A mixin for classes that share some method implementations."""
67-
68- def getRootURL(self, rootsite):
69- """See IBasicLaunchpadRequest."""
70- if rootsite is not None:
71- assert rootsite in allvhosts.configs, (
72- "rootsite is %s. Must be in %r." % (
73- rootsite, sorted(allvhosts.configs.keys())))
74- root_url = allvhosts.configs[rootsite].rooturl
75- else:
76- root_url = self.getApplicationURL() + '/'
77- return root_url
78-
79-
80 class LaunchpadBrowserRequest(BasicLaunchpadRequest, BrowserRequest,
81- NotificationRequest, ErrorReportRequest,
82- LaunchpadBrowserRequestMixin):
83+ NotificationRequest, ErrorReportRequest):
84 """Integration of launchpad mixin request classes to make an uber
85 launchpad request class.
86 """
87@@ -1370,7 +1374,7 @@
88
89
90 class PublicXMLRPCRequest(BasicLaunchpadRequest, XMLRPCRequest,
91- ErrorReportRequest, LaunchpadBrowserRequestMixin):
92+ ErrorReportRequest):
93 """Request type for doing public XML-RPC in Launchpad."""
94
95 def _createResponse(self):
96
97=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
98--- lib/canonical/launchpad/webapp/tests/test_servers.py 2010-08-20 20:31:18 +0000
99+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2010-09-28 07:13:54 +0000
100@@ -358,6 +358,20 @@
101 retried_request.response.getHeader('Vary'),
102 'Cookie, Authorization')
103
104+ def test_is_ajax_false(self):
105+ """Normal requests do not define HTTP_X_REQUESTED_WITH."""
106+ request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})
107+
108+ self.assertFalse(request.is_ajax)
109+
110+ def test_is_ajax_true(self):
111+ """Requests with HTTP_X_REQUESTED_WITH set are ajax requests."""
112+ request = LaunchpadBrowserRequest(StringIO.StringIO(''), {
113+ 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
114+ })
115+
116+ self.assertTrue(request.is_ajax)
117+
118
119 class IThingSet(Interface):
120 """Marker interface for a set of things."""
121
122=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
123--- lib/lp/registry/browser/distroseriesdifference.py 2010-09-17 09:48:32 +0000
124+++ lib/lp/registry/browser/distroseriesdifference.py 2010-09-28 07:13:54 +0000
125@@ -8,18 +8,59 @@
126 'DistroSeriesDifferenceView',
127 ]
128
129-from zope.interface import implements
130+from zope.app.form.browser.itemswidgets import RadioWidget
131+from zope.interface import (
132+ implements,
133+ Interface,
134+ )
135+from zope.schema import Choice
136+from zope.schema.vocabulary import (
137+ SimpleTerm,
138+ SimpleVocabulary,
139+ )
140
141-from canonical.launchpad.webapp.publisher import LaunchpadView
142+from canonical.launchpad.webapp import LaunchpadFormView
143+from canonical.launchpad.webapp.authorization import check_permission
144+from canonical.launchpad.webapp.launchpadform import custom_widget
145+from lp.registry.enum import DistroSeriesDifferenceStatus
146 from lp.services.comments.interfaces.conversation import (
147 IComment,
148 IConversation,
149 )
150
151
152-class DistroSeriesDifferenceView(LaunchpadView):
153+class IDistroSeriesDifferenceForm(Interface):
154+ """An interface used in the browser only for displaying form elements."""
155+ blacklist_options = Choice(vocabulary=SimpleVocabulary((
156+ SimpleTerm('NONE', 'NONE', 'No'),
157+ SimpleTerm(
158+ DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
159+ DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS.name,
160+ 'All versions'),
161+ SimpleTerm(
162+ DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
163+ DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT.name,
164+ 'This version'),
165+ )))
166+
167+
168+class DistroSeriesDifferenceView(LaunchpadFormView):
169
170 implements(IConversation)
171+ schema = IDistroSeriesDifferenceForm
172+ custom_widget('blacklist_options', RadioWidget)
173+
174+ @property
175+ def initial_values(self):
176+ """Ensure the correct radio button is checked for blacklisting."""
177+ blacklisted_statuses = (
178+ DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
179+ DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
180+ )
181+ if self.context.status in blacklisted_statuses:
182+ return dict(blacklist_options=self.context.status)
183+
184+ return dict(blacklist_options='NONE')
185
186 @property
187 def binary_summaries(self):
188@@ -40,11 +81,16 @@
189 @property
190 def comments(self):
191 """See `IConversation`."""
192- # Could use a generator here?
193 return [
194 DistroSeriesDifferenceDisplayComment(comment) for
195 comment in self.context.getComments()]
196
197+ @property
198+ def show_blacklist_options(self):
199+ """Only show the options if an editor requests via JS."""
200+ return self.request.is_ajax and check_permission(
201+ 'launchpad.Edit', self.context)
202+
203
204 class DistroSeriesDifferenceDisplayComment:
205 """Used simply to provide `IComment` for rendering."""
206
207=== modified file 'lib/lp/registry/browser/tests/test_distroseriesdifference_views.py'
208--- lib/lp/registry/browser/tests/test_distroseriesdifference_views.py 2010-09-17 09:48:32 +0000
209+++ lib/lp/registry/browser/tests/test_distroseriesdifference_views.py 2010-09-28 07:13:54 +0000
210@@ -10,6 +10,7 @@
211 from BeautifulSoup import BeautifulSoup
212 from zope.component import getUtility
213
214+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
215 from canonical.launchpad.webapp.testing import verifyObject
216 from canonical.testing import (
217 DatabaseFunctionalLayer,
218@@ -18,14 +19,17 @@
219 from lp.registry.browser.distroseriesdifference import (
220 DistroSeriesDifferenceDisplayComment,
221 )
222-from lp.registry.enum import DistroSeriesDifferenceType
223-from lp.registry.interfaces.distroseriesdifference import IDistroSeriesDifferenceSource
224+from lp.registry.enum import (
225+ DistroSeriesDifferenceStatus,
226+ DistroSeriesDifferenceType,
227+ )
228+from lp.registry.interfaces.distroseriesdifference import (
229+ IDistroSeriesDifferenceSource)
230 from lp.services.comments.interfaces.conversation import (
231 IComment,
232 IConversation,
233 )
234 from lp.soyuz.enums import PackagePublishingStatus
235-from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
236 from lp.testing import (
237 celebrity_logged_in,
238 person_logged_in,
239@@ -57,6 +61,8 @@
240
241 def addSummaryToDifference(self, distro_series_difference):
242 """Helper that adds binaries with summary info to the source pubs."""
243+ # Avoid circular import.
244+ from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
245 distro_series = distro_series_difference.derived_series
246 source_package_name_str = (
247 distro_series_difference.source_package_name.name)
248@@ -124,6 +130,36 @@
249 self.assertIs(None, ds_diff.source_pub)
250 self.assertIs(None, view.binary_summaries)
251
252+ def test_show_blacklist_options_non_ajax(self):
253+ # Blacklist options are not shown for non-ajax requests.
254+ ds_diff = self.factory.makeDistroSeriesDifference()
255+
256+ # Without JS, even editors don't see blacklist options.
257+ with person_logged_in(ds_diff.owner):
258+ view = create_initialized_view(
259+ ds_diff, '+listing-distroseries-extra')
260+ self.assertFalse(view.show_blacklist_options)
261+
262+ def test_show_blacklist_options_editor(self):
263+ # Blacklist options are shown if requested by an editor via
264+ # ajax.
265+ ds_diff = self.factory.makeDistroSeriesDifference()
266+
267+ request = LaunchpadTestRequest(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
268+ with person_logged_in(ds_diff.owner):
269+ view = create_initialized_view(
270+ ds_diff, '+listing-distroseries-extra', request=request)
271+ self.assertTrue(view.show_blacklist_options)
272+
273+ def test_show_blacklist_options_non_editor(self):
274+ # Even with a JS request, non-editors do not see the options.
275+ ds_diff = self.factory.makeDistroSeriesDifference()
276+
277+ request = LaunchpadTestRequest(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
278+ view = create_initialized_view(
279+ ds_diff, '+listing-distroseries-extra', request=request)
280+ self.assertFalse(view.show_blacklist_options)
281+
282
283 class DistroSeriesDifferenceTemplateTestCase(TestCaseWithFactory):
284
285@@ -215,3 +251,43 @@
286 1, len(soup.findAll('pre', text="I'm working on this.")))
287 self.assertEqual(
288 1, len(soup.findAll('pre', text="Here's another comment.")))
289+
290+ def test_blacklist_options(self):
291+ # blacklist options are presented to editors.
292+ ds_diff = self.factory.makeDistroSeriesDifference()
293+
294+ with person_logged_in(ds_diff.owner):
295+ request = LaunchpadTestRequest(
296+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
297+ view = create_initialized_view(
298+ ds_diff, '+listing-distroseries-extra', request=request)
299+ soup = BeautifulSoup(view())
300+
301+ self.assertEqual(
302+ 1, len(soup.findAll('div', {'class': 'blacklist-options'})))
303+
304+ def test_blacklist_options_initial_values_NONE(self):
305+ ds_diff = self.factory.makeDistroSeriesDifference()
306+ view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
307+
308+ # If the difference is not currently blacklisted, 'NONE' is set
309+ # as the default value for the field.
310+ self.assertEqual('NONE', view.initial_values.get('blacklist_options'))
311+
312+ def test_blacklist_options_initial_values_CURRENT(self):
313+ ds_diff = self.factory.makeDistroSeriesDifference(
314+ status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
315+ view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
316+
317+ self.assertEqual(
318+ DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
319+ view.initial_values.get('blacklist_options'))
320+
321+ def test_blacklist_options_initial_values_ALWAYS(self):
322+ ds_diff = self.factory.makeDistroSeriesDifference(
323+ status=DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS)
324+ view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
325+
326+ self.assertEqual(
327+ DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
328+ view.initial_values.get('blacklist_options'))
329
330=== modified file 'lib/lp/registry/browser/tests/test_distroseriesdifference_webservice.py'
331--- lib/lp/registry/browser/tests/test_distroseriesdifference_webservice.py 2010-09-21 11:17:25 +0000
332+++ lib/lp/registry/browser/tests/test_distroseriesdifference_webservice.py 2010-09-28 07:13:54 +0000
333@@ -59,3 +59,28 @@
334 self.assertEqual(
335 DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
336 ds_diff.status)
337+
338+ def test_unblacklist_not_public(self):
339+ # The unblacklist method is not publically available.
340+ ds_diff = self.factory.makeDistroSeriesDifference(
341+ status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
342+ ws_diff = ws_object(self.factory.makeLaunchpadService(), ds_diff)
343+
344+ self.assertRaises(Unauthorized, ws_diff.unblacklist)
345+
346+ def test_unblacklist(self):
347+ # The unblacklist method can be called by people with edit access.
348+ ds_diff = self.factory.makeDistroSeriesDifference(
349+ status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
350+ ws_diff = ws_object(self.factory.makeLaunchpadService(
351+ ds_diff.owner), ds_diff)
352+
353+ result = ws_diff.unblacklist()
354+ transaction.commit()
355+
356+ utility = getUtility(IDistroSeriesDifferenceSource)
357+ ds_diff = utility.getByDistroSeriesAndName(
358+ ds_diff.derived_series, ds_diff.source_package_name.name)
359+ self.assertEqual(
360+ DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
361+ ds_diff.status)
362
363=== modified file 'lib/lp/registry/browser/tests/test_series_views.py'
364--- lib/lp/registry/browser/tests/test_series_views.py 2010-09-21 08:42:29 +0000
365+++ lib/lp/registry/browser/tests/test_series_views.py 2010-09-28 07:13:54 +0000
366@@ -73,15 +73,6 @@
367 name=derived_name, parent_series=parent)
368 return derived_series
369
370- def makeControllerInScopes(self, scopes):
371- """Make a controller that will report it's in the given scopes."""
372- call_log = []
373-
374- def scope_cb(scope):
375- call_log.append(scope)
376- return scope in scopes
377- return FeatureController(scope_cb), call_log
378-
379 def setDerivedSeriesUIFeatureFlag(self):
380 # Helper to set the feature flag enabling the derived series ui.
381 ignore = getFeatureStore().add(FeatureFlag(
382
383=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
384--- lib/lp/registry/interfaces/distroseriesdifference.py 2010-09-20 15:00:11 +0000
385+++ lib/lp/registry/interfaces/distroseriesdifference.py 2010-09-28 07:13:54 +0000
386@@ -17,10 +17,12 @@
387 export_as_webservice_entry,
388 export_write_operation,
389 exported,
390+ operation_parameters,
391 )
392 from lazr.restful.fields import Reference
393 from zope.interface import Interface
394 from zope.schema import (
395+ Bool,
396 Choice,
397 Int,
398 TextLine,
399@@ -135,6 +137,8 @@
400 def addComment(owner, comment):
401 """Add a comment on this difference."""
402
403+ @operation_parameters(
404+ all=Bool(title=_("All"), required=False))
405 @export_write_operation()
406 def blacklist(all=False):
407 """Blacklist this version or all versions of this source package.
408@@ -143,6 +147,13 @@
409 be blacklisted or just the current (default).
410 """
411
412+ @export_write_operation()
413+ def unblacklist():
414+ """Removes this difference from the blacklist.
415+
416+ The status will be updated based on the versions.
417+ """
418+
419
420 class IDistroSeriesDifference(IDistroSeriesDifferencePublic,
421 IDistroSeriesDifferenceEdit):
422
423=== modified file 'lib/lp/registry/javascript/distroseriesdifferences_details.js'
424--- lib/lp/registry/javascript/distroseriesdifferences_details.js 2010-09-15 15:05:11 +0000
425+++ lib/lp/registry/javascript/distroseriesdifferences_details.js 2010-09-28 07:13:54 +0000
426@@ -11,40 +11,130 @@
427
428 var namespace = Y.namespace('lp.registry.distroseriesdifferences_details');
429
430+/**
431+ * Create one Launchpad client that will be used with multiple requests.
432+ */
433+var lp_client = new LP.client.Launchpad();
434+
435 /*
436 * Setup the expandable rows for each difference.
437 *
438 * @method setup_expandable_rows
439 */
440 namespace.setup_expandable_rows = function() {
441- var start_update = function(uri, container) {
442+
443+ var blacklist_handler = function(e, api_uri, source_name) {
444+ // We only want to select the new radio if the update is
445+ // successful.
446+ e.preventDefault();
447+ var blacklist_options_container = this.ancestor('div');
448+
449+ // Disable all the inputs
450+ blacklist_options_container.all('input').set('disabled', 'disabled');
451+ e.target.prepend('<img src="/@@/spinner" />');
452+
453+ var method_name = (e.target.get('value') == 'NONE') ?
454+ 'unblacklist' : 'blacklist';
455+ var blacklist_all = (e.target.get('value') == 'BLACKLISTED_ALWAYS');
456+
457+ var diff_rows = Y.all('tr.' + source_name);
458+
459+ var config = {
460+ on: {
461+ success: function(updated_entry, args) {
462+ // Let the user know this item is now blacklisted.
463+ blacklist_options_container.one('img').remove();
464+ blacklist_options_container.all(
465+ 'input').set('disabled', false);
466+ e.target.set('checked', true);
467+ Y.each(diff_rows, function(diff_row) {
468+ var fade_to_gray = new Y.Anim({
469+ node: diff_row,
470+ from: { backgroundColor: '#FFFFFF'},
471+ to: { backgroundColor: '#EEEEEE'}
472+ });
473+ if (method_name == 'unblacklist') {
474+ fade_to_gray.set('reverse', true);
475+ }
476+ fade_to_gray.run();
477+ });
478+ },
479+ failure: function(id, response) {
480+ blacklist_options_container.one('img').remove();
481+ blacklist_options_container.all(
482+ 'input').set('disabled', false);
483+ }
484+ },
485+ parameters: {
486+ all: blacklist_all
487+ }
488+ };
489+
490+ lp_client.named_post(api_uri, method_name, config);
491+
492+ };
493+
494+ /**
495+ * Link the click event for these blacklist options to the correct
496+ * api uri.
497+ *
498+ * @param blacklist_options {Node} The node containing the blacklist
499+ * options.
500+ * @param source_name {string} The name of the source to update.
501+ */
502+ var setup_blacklist_options = function(blacklist_options,
503+ source_name) {
504+ var api_uri = [
505+ LP.client.cache.context.self_link,
506+ '+difference',
507+ source_name
508+ ].join('/')
509+ Y.on('click', blacklist_handler, blacklist_options.all('input'),
510+ blacklist_options, api_uri, source_name);
511+ };
512+
513+ /**
514+ * Get the extra information for this diff to display.
515+ *
516+ * @param uri {string} The uri for the extra diff info.
517+ * @param container {Node} A node which must contain a div with the
518+ * class 'diff-extra-container' into which the results
519+ * are inserted.
520+ */
521+ var get_extra_diff_info = function(uri, container, source_name) {
522
523 var in_progress_message = Y.lp.soyuz.base.makeInProgressNode(
524 'Fetching difference details ...')
525- container.insert(in_progress_message, 'replace');
526+ container.one('div.diff-extra-container').insert(
527+ in_progress_message, 'replace');
528+
529+ var success_cb = function(transaction_id, response, args) {
530+ args.container.one('div.diff-extra-container').insert(
531+ response.responseText, 'replace');
532+ setup_blacklist_options(args.container.one(
533+ 'div.blacklist-options'), source_name);
534+ };
535+
536+ var failure_cb = function(transaction_id, response, args){
537+ var retry_handler = function(e) {
538+ e.preventDefault();
539+ get_extra_diff_info(args.uri, args.container);
540+ };
541+ var failure_message = Y.lp.soyuz.base.makeFailureNode(
542+ 'Failed to fetch difference details.',
543+ retry_handler);
544+ args.container.insert(failure_message, 'replace');
545+
546+ var anim = Y.lazr.anim.red_flash({
547+ node: args.container
548+ });
549+ anim.run();
550+ };
551
552 var config = {
553 on: {
554- 'success': function(transaction_id, response, args) {
555- args.container.set(
556- 'innerHTML', response.responseText);
557- // Change to insert(,'replace)
558- },
559- 'failure': function(transaction_id, response, args){
560- var retry_handler = function(e) {
561- e.preventDefault();
562- start_update(args.uri, args.container);
563- };
564- var failure_message = Y.lp.soyuz.base.makeFailureNode(
565- 'Failed to fetch difference details.',
566- retry_handler);
567- args.container.insert(failure_message, 'replace');
568-
569- var anim = Y.lazr.anim.red_flash({
570- node: args.container
571- });
572- anim.run();
573- }
574+ 'success': success_cb,
575+ 'failure': failure_cb,
576 },
577 arguments: {
578 'container': container,
579@@ -64,18 +154,22 @@
580 // Only insert if there isn't already a container row there.
581 next_row = row.next();
582 if (next_row == null || !next_row.hasClass('diff-extra')) {
583- details_row = Y.Node.create(
584- '<table><tr class="diff-extra unseen"><td colspan="5"></td></tr></table>').one('tr');
585+ var source_name = row.one('a.toggle-extra').get('text');
586+ var details_row = Y.Node.create([
587+ '<table><tr class="diff-extra unseen ' + source_name + '">',
588+ ' <td colspan="5">',
589+ ' <div class="diff-extra-container"></div>',
590+ ' </td></tr></table>'
591+ ].join('')).one('tr');
592 row.insert(details_row, 'after');
593 var uri = toggle.get('href');
594- start_update(uri, details_row.one('td'));
595+ get_extra_diff_info(uri, details_row.one('td'), source_name);
596 } else {
597 details_row = next_row
598 }
599-
600 details_row.toggleClass('unseen');
601+ };
602
603- };
604 Y.all('table.listing a.toggle-extra').each(function(toggle){
605 toggle.addClass('treeCollapsed').addClass('sprite');
606 toggle.on("click", expander_handler);
607@@ -83,4 +177,4 @@
608
609 };
610
611-}, "0.1", {"requires": ["io-base", "lp.soyuz.base"]});
612+}, "0.1", {"requires": ["io-base", "lp.soyuz.base", "lazr.anim"]});
613
614=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
615--- lib/lp/registry/model/distroseriesdifference.py 2010-09-21 11:17:25 +0000
616+++ lib/lp/registry/model/distroseriesdifference.py 2010-09-28 07:13:54 +0000
617@@ -272,3 +272,8 @@
618 self.status = DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS
619 else:
620 self.status = DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT
621+
622+ def unblacklist(self):
623+ """See `IDistroSeriesDifference`."""
624+ self.status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
625+ self.update()
626
627=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
628--- lib/lp/registry/templates/distroseries-localdifferences.pt 2010-09-16 09:32:35 +0000
629+++ lib/lp/registry/templates/distroseries-localdifferences.pt 2010-09-28 07:13:54 +0000
630@@ -44,7 +44,8 @@
631 <tal:difference repeat="difference differences/batch">
632 <tr tal:define="parent_source_pub difference/parent_source_pub;
633 source_pub difference/source_pub;
634- signer source_pub/sourcepackagerelease/uploader/fmt:link|nothing;">
635+ signer source_pub/sourcepackagerelease/uploader/fmt:link|nothing;"
636+ tal:attributes="class parent_source_pub/source_package_name">
637 <td><a tal:attributes="href difference/fmt:url" class="toggle-extra"
638 tal:content="parent_source_pub/source_package_name">Foo</a>
639 </td>
640@@ -52,7 +53,8 @@
641 <tal:replace
642 replace="parent_source_pub/sourcepackagerelease/version"/></a>
643 </td>
644- <td><a tal:attributes="href source_pub/sourcepackagerelease/fmt:url">
645+ <td><a tal:attributes="href source_pub/sourcepackagerelease/fmt:url"
646+ class="derived-version">
647 <tal:replace
648 replace="source_pub/sourcepackagerelease/version"/></a>
649 </td>
650
651=== modified file 'lib/lp/registry/templates/distroseriesdifference-listing-extra.pt'
652--- lib/lp/registry/templates/distroseriesdifference-listing-extra.pt 2010-09-17 09:48:32 +0000
653+++ lib/lp/registry/templates/distroseriesdifference-listing-extra.pt 2010-09-28 07:13:54 +0000
654@@ -3,6 +3,18 @@
655 xmlns:metal="http://xml.zope.org/namespaces/metal"
656 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
657 i18n:domain="launchpad">
658+
659+ <tal:show_options condition="view/show_blacklist_options">
660+ <div class="blacklist-options" style="float:right">
661+ <dl>
662+ <dt>Blacklisted:</dt>
663+ <dd>
664+ <form><tal:replace replace="structure view/widgets/blacklist_options" /></form>
665+ </dd>
666+ </dl>
667+ </div>
668+ </tal:show_options>
669+
670 <dl>
671 <dt>Binary descriptions:</dt>
672 <dd><ul>
673
674=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
675--- lib/lp/registry/tests/test_distroseriesdifference.py 2010-09-20 15:00:11 +0000
676+++ lib/lp/registry/tests/test_distroseriesdifference.py 2010-09-28 07:13:54 +0000
677@@ -369,6 +369,39 @@
678 DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
679 ds_diff.status)
680
681+ def test_unblacklist(self):
682+ # Unblacklisting will return to NEEDS_ATTENTION by default.
683+ ds_diff = self.factory.makeDistroSeriesDifference(
684+ status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
685+
686+ with person_logged_in(ds_diff.owner):
687+ ds_diff.unblacklist()
688+
689+ self.assertEqual(
690+ DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
691+ ds_diff.status)
692+
693+ def test_unblacklist_resolved(self):
694+ # Status is resolved when unblacklisting a now-resolved difference.
695+ ds_diff = self.factory.makeDistroSeriesDifference(
696+ versions={
697+ 'derived': '0.9',
698+ 'parent': '1.0',
699+ },
700+ status=DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS)
701+ new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
702+ sourcepackagename=ds_diff.source_package_name,
703+ distroseries=ds_diff.derived_series,
704+ status=PackagePublishingStatus.PENDING,
705+ version='1.0')
706+
707+ with person_logged_in(ds_diff.owner):
708+ ds_diff.unblacklist()
709+
710+ self.assertEqual(
711+ DistroSeriesDifferenceStatus.RESOLVED,
712+ ds_diff.status)
713+
714 def test_source_package_name_unique_for_derived_series(self):
715 # We cannot create two differences for the same derived series
716 # for the same package.
717
718=== modified file 'lib/lp/registry/windmill/tests/test_distroseriesdifference_expander.py'
719--- lib/lp/registry/windmill/tests/test_distroseriesdifference_expander.py 2010-09-16 14:02:30 +0000
720+++ lib/lp/registry/windmill/tests/test_distroseriesdifference_expander.py 2010-09-28 07:13:54 +0000
721@@ -1,10 +1,15 @@
722 # Copyright 2010 Canonical Ltd. This software is licensed under the
723 # GNU Affero General Public License version 3 (see the file LICENSE).
724
725+from zope.component import getUtility
726+
727 import transaction
728
729 from canonical.launchpad.webapp.publisher import canonical_url
730 from canonical.launchpad.windmill.testing import constants
731+from canonical.launchpad.windmill.testing import lpuser
732+from lp.registry.enum import DistroSeriesDifferenceStatus
733+from lp.registry.interfaces.distroseriesdifference import IDistroSeriesDifferenceSource
734 from lp.registry.windmill.testing import RegistryWindmillLayer
735 from lp.services.features.model import FeatureFlag, getFeatureStore
736 from lp.testing import WindmillTestCase
737@@ -32,11 +37,36 @@
738 self.package_diffs_url = (
739 canonical_url(self.diff.derived_series) + '/+localpackagediffs')
740
741- def test_diff_extra_details_available(self):
742+ def test_diff_extra_details_blacklisting(self):
743 """A successful request for the extra info updates the display."""
744+ #login_person(self.diff.owner, 'test', self.client)
745+ lpuser.FOO_BAR.ensure_login(self.client)
746 self.client.open(url=self.package_diffs_url)
747 self.client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
748 self.client.click(link=u'foo')
749 self.client.waits.forElement(
750 classname=u'diff-extra', timeout=constants.FOR_ELEMENT)
751
752+ self.client.click(id=u'field.blacklist_options.1')
753+ self.client.waits.forElementProperty(
754+ option=u'enabled', id=u'field.blacklist_options.1')
755+
756+ # Reload the diff and ensure it's been updated.
757+ transaction.commit()
758+ diff_source = getUtility(IDistroSeriesDifferenceSource)
759+ diff_reloaded = diff_source.getByDistroSeriesAndName(
760+ self.diff.derived_series, 'foo')
761+ self.assertEqual(
762+ DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
763+ diff_reloaded.status)
764+
765+ # Now set it back so that it's not blacklisted.
766+ self.client.click(id=u'field.blacklist_options.0')
767+ self.client.waits.forElementProperty(
768+ option=u'enabled', id=u'field.blacklist_options.0')
769+ transaction.commit()
770+ diff_reloaded = diff_source.getByDistroSeriesAndName(
771+ self.diff.derived_series, 'foo')
772+ self.assertEqual(
773+ DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
774+ diff_reloaded.status)

Subscribers

People subscribed via source and target branches

to status/vote changes: