Merge lp:~michael.nelson/launchpad/644328-blacklist-ui into lp:launchpad/db-devel
- 644328-blacklist-ui
- Merge into db-devel
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 | ||||
Related bugs: |
|
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 distroseriesdif
Description of the change
Overview
========
This branch implements the UI for blacklisting source package differences for derived distro series.
See the LEP here:
https:/
and the specific mockup here:
https:/
You can view a 1min demo of the UI here:
http://
updated after salgado's ui review to:
http://
(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_distroseri
To demo locally:
================
Run http://
https:/
Michael Nelson (michael.nelson) wrote : | # |
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:/
<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
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.
Michael Nelson (michael.nelson) wrote : | # |
Hi Guilherme,
Here's an updated screencast based on your UI review:
http://
Let me know if it's what you were thinking.
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.
Guilherme Salgado (salgado) : | # |
Curtis Hovey (sinzui) wrote : | # |
This looks great. Thanks Michael and Salgado for doing such a good job on this.
Henning Eggers (henninge) wrote : | # |
-----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/
> === modified file 'lib/canonical/
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -526,7 +526,27 @@
> return decoded_qs
>
>
> -class BasicLaunchpadR
> +class LaunchpadBrowse
> + """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 IBasicLaunchpad
> + if rootsite is not None:
> + assert rootsite in allvhosts.configs, (
> + "rootsite is %s. Must be in %r." % (
> + rootsite, sorted(
> + root_url = allvhosts.
> + else:
> + root_url = self.getApplica
> + return root_url
> +
> + @property
> + def is_ajax(self):
> + """See `IBasicLaunchpa
> + return 'XMLHttpRequest' == self.getHeader(
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 BasicLaunchpadR
> """Mixin request class to provide stepstogo."""
>
> implements(
> @@ -581,24 +601,8 @@
> return get_query_
>
>
> -class LaunchpadBrowse
> - """A mixin for classes that share some method implementations."""
> -
> - def getRootURL(self, rootsite):
> - """See IBasicLaunchpad
> - if rootsite is not None:
> - assert rootsite in allvhosts.configs, (
> - "rootsite is %s. Must be in %r." % (
> - rootsite, sorted(
> - root_url = allvhosts.
> - else:
> - root_url = self.getApplica
> - return root_url
> -
> -
> class LaunchpadBrowse
> - NotificationReq
> - LaunchpadBrowse
> + NotificationReq
> """Integration of launchpad mixin request classes to make an uber
> launchpad request class.
> """
> @@ -1370,7 +1374,7 @@
>
>
> class PublicXMLRPCReq
> - ErrorReportRequest, LaunchpadBrowse
> + ErrorReportRequ
> """Request type for doing public XML-RPC in Launchpad."""
>
> def _createResponse
>
...
Michael Nelson (michael.nelson) wrote : | # |
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/
>> --- lib/canonical/
>> +++ lib/canonical/
>> @@ -526,7 +526,27 @@
>> Â Â Â return decoded_qs
>>
>>
>> -class BasicLaunchpadR
>> +class LaunchpadBrowse
>> + Â Â """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 `IBasicLaunchpa
>> + Â Â Â Â return 'XMLHttpRequest' == self.getHeader(
>
> 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/
>> --- lib/canonical/
>> +++ lib/canonical/
>> @@ -358,6 +358,16 @@
>> Â Â Â Â Â Â Â retried_
>> Â Â Â Â Â Â Â 'Cookie, Authorization')
>>
>> + Â Â def test_is_ajax(self):
>
> Please split this test up:
> Â Â Â def test_is_
Done (test_is_
>> Â Â Â implements(
>> + Â Â schema = IDistroSeriesDi
>> + Â Â custom_
>> +
>> + Â Â @property
>> + Â Â def initial_
>> + Â Â Â Â """Ensure the correct radio button is checked for blacklisting."""
>> + Â Â Â Â if self.context.status in (
>> + Â Â Â Â Â Â DistroSeriesDif
>> + Â Â Â Â Â Â DistroSeriesDif
>> + Â Â Â Â Â Â ):
>
> 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 are shown if requested by an editor via
>> + Â Â Â Â # ajax.
>> + Â Â Â Â ds_diff = self.factory.
>> +
>> + Â Â Â Â # Without JS, even editors don't see blacklist options.
>> + Â Â Â Â with person_
>> + Â Â Â Â Â Â view = create_
>> + Â Â Â Â Â Â Â Â ds_diff, '+listing-
>> + Â Â Â Â self.assertFals
>
> 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
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) |
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.