Merge lp:~michael.nelson/launchpad/409187-trivial-ui-fixes-for-p3a-access into lp:launchpad

Proposed by Michael Nelson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~michael.nelson/launchpad/409187-trivial-ui-fixes-for-p3a-access
Merge into: lp:launchpad
Diff against target: 932 lines
19 files modified
lib/canonical/launchpad/icing/style-3-0.css (+3/-0)
lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js (+54/-0)
lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html (+46/-0)
lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js (+147/-0)
lib/canonical/launchpad/pagetitles.py (+0/-8)
lib/lp/soyuz/browser/archivesubscription.py (+28/-3)
lib/lp/soyuz/browser/distroarchseriesbinarypackage.py (+0/-9)
lib/lp/soyuz/browser/tests/archivesubscription-views.txt (+23/-7)
lib/lp/soyuz/browser/tests/test_breadcrumbs.py (+34/-0)
lib/lp/soyuz/configure.zcml (+6/-1)
lib/lp/soyuz/interfaces/archivesubscriber.py (+3/-0)
lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt (+3/-3)
lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt (+1/-0)
lib/lp/soyuz/templates/archive-subscriber-edit.pt (+0/-6)
lib/lp/soyuz/templates/archive-subscribers.pt (+19/-13)
lib/lp/soyuz/templates/person-archive-subscription.pt (+9/-14)
lib/lp/soyuz/templates/person-archive-subscriptions.pt (+4/-8)
lib/lp/soyuz/windmill/testing.py (+18/-0)
lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py (+96/-0)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/409187-trivial-ui-fixes-for-p3a-access
Reviewer Review Type Date Requested Status
Graham Binns (community) js Approve
Barry Warsaw (community) ui* Approve
Guilherme Salgado (community) Approve
Review via email: mp+12035@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :

= Summary =

This branch deals with a number of UI issues related to private PPA
subscriptions. Please refer tot bug 409187 for the details.

== Proposed fix ==

== Pre-implementation notes ==

All before-after screenshots and notes are on bug 409187, as well as a
screencast of the new interaction.

== Implementation details ==

I have not added a windmill test for the new interaction yet, but will
do soon. If I need to do it now I will, but otherwise I'll focus on
blueprint templates for the rest of today.

== Tests ==

bin/test -vv -t archivesubscription-views.txt -t
TestArchiveSubscriptionBreadcrumb -t stories/ppa

== Demo and Q/A ==

For screenshots/casts, see bug 409187.

To demo locally, run the following in a harness:

ppa = getUtility(IPersonSet).getByName('cprov').archive
ppa.private = True
ppa.buildd_secret = 'blah'
import transaction;transaction.commit()

and then navigate to:
https://launchpad.dev/~cprov/+archive/ppa

Click on 'Manage access', add yourself (Celso) and others. Try editing.

Next go to:
https://launchpad.dev/~cprov and click on 'View your private ppa
subscriptions'. Click on the subscription listed there etc.

For QA, the soyuz-team ppa can be used.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/soyuz/templates/person-archive-subscriptions.pt
  lib/lp/soyuz/templates/archive-subscribers.pt
  lib/canonical/launchpad/pagetitles.py
  lib/lp/soyuz/templates/person-archive-subscription.pt
  lib/lp/soyuz/browser/tests/test_breadcrumbs.py
  lib/lp/soyuz/browser/tests/archivesubscription-views.txt
  lib/lp/soyuz/interfaces/archivesubscriber.py
  lib/lp/soyuz/configure.zcml
  lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt
  lib/canonical/launchpad/icing/style-3-0.css
  lib/lp/soyuz/browser/archivesubscription.py
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/soyuz/templates/archive-subscriber-edit.pt

== Pylint notices ==

lib/lp/soyuz/interfaces/archivesubscriber.py
    20: [F0401] Unable to import 'lazr.enum' (No module named enum)
    26: [F0401] Unable to import 'lazr.restful.declarations' (No module
named restful)
    27: [F0401] Unable to import 'lazr.restful.fields' (No module named
restful)

--
Michael

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (9.0 KiB)

Hi Michael,

Your changes look good. I have a few suggestions and I'd like somebody
else to review the JS changes, as I don't feel comfortable doing so.

 review approve

> === modified file 'lib/lp/soyuz/browser/archivesubscription.py'
> --- lib/lp/soyuz/browser/archivesubscription.py 2009-08-12 08:40:41 +0000
> +++ lib/lp/soyuz/browser/archivesubscription.py 2009-09-14 08:08:51 +0000
> @@ -9,6 +9,8 @@
>
> __all__ = [
> 'ArchiveSubscribersView',
> + 'PersonalArchiveSubscriptionBreadcrumb',
> + 'PersonArchiveSubscriptionView',
> 'PersonArchiveSubscriptionsView',
> 'traverse_archive_subscription_for_subscriber'
> ]
> @@ -34,10 +36,11 @@
> IArchiveAuthTokenSet)
> from lp.soyuz.interfaces.archivesubscriber import (
> IArchiveSubscriberSet, IPersonalArchiveSubscription)
> +from canonical.launchpad.webapp.breadcrumb import Breadcrumb
> from canonical.launchpad.webapp.launchpadform import (
> action, custom_widget, LaunchpadFormView, LaunchpadEditFormView)
> from canonical.launchpad.webapp.publisher import (
> - canonical_url, LaunchpadView)
> + canonical_url, LaunchpadView, Navigation)
> from canonical.widgets import DateWidget
> from canonical.widgets.popup import PersonPickerWidget
>
> @@ -65,6 +68,20 @@
> """See `IPersonalArchiveSubscription`."""
> return "Access to %s" % self.archive.displayname
>
> + @property
> + def title(self):

I know of page_title and label, which are used for this purpose, did you mean
one of them or am I not aware of this third one?

> + """Required for default headings in templates."""
> + return self.displayname
> +
> +
> +class PersonalArchiveSubscriptionNavigation(Navigation):
> + """Navigation for `IPersonalArchiveSubscription`.
> +
> + Without this, a breadcrumb will not be created for personal
> + archive subscription objects.

This is no longer needed, actually -- a fix for the bug (423898) is included
in my breadcrumbs branch that is on PQM now.

> + """
> + usedfor = IPersonalArchiveSubscription
> +
> def traverse_archive_subscription_for_subscriber(subscriber, archive_id):
> """Return the subscription for a subscriber to an archive."""
> subscription = None
> @@ -79,6 +96,14 @@
> return PersonalArchiveSubscription(subscriber, archive)
>
>
> +class PersonalArchiveSubscriptionBreadcrumb(Breadcrumb):
> + """Builds a breadcrumb for `PersonalArchiveSubscription`."""
> +
> + @property
> + def text(self):
> + return self.context.displayname

You can use webapp.breadcrumbs.DisplaynameBreadcrumb in the zcml and get rid
of this adapter altogether. It'd be great if you could do a quick check for
other places where that generic adapter could replace other specific ones. :)

> +
> +
> class IArchiveSubscriberUI(Interface):
> """A custom interface for user interaction with archive subscriptions.
>
> === modified file 'lib/lp/soyuz/browser/tests/test_breadcrumbs.py'
> --- lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-02 16:32:06 +0000
> +++ lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-11 10:52:54 +0000
> @@ -11,6 +11,8 @@
> from canonical.launc...

Read more...

review: Approve
Revision history for this message
Barry Warsaw (barry) wrote :

Looks very good, and the movie really helped! The only thing that's weird for me is leaving the date blank to mean never expire. There's no need to hold up the branch for this, so I'll just ramble a bit.

If possible, the date picker maybe should have a "Never expire" button. You'd use this both in the initial date choice but also if you then decide to extend a user's access to "forever". Also, if access never expires, it would be nice if the date field actually said that, instead of being blank.

With these changes, you'd be able to get rid of the helpful text explaining what a blank field means.

If you think these are worthwhile changes to make, file a bug and fix it later. But the ui as it is looks otherwise great.

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

Guilherme Salgado wrote:
> Review: Approve
> Hi Michael,
>
> Your changes look good. I have a few suggestions and I'd like somebody
> else to review the JS changes, as I don't feel comfortable doing so.
>

Great, thanks Salgado.

> review approve
>
>> === modified file 'lib/lp/soyuz/browser/archivesubscription.py'
>> --- lib/lp/soyuz/browser/archivesubscription.py 2009-08-12 08:40:41 +0000
>> +++ lib/lp/soyuz/browser/archivesubscription.py 2009-09-14 08:08:51 +0000
>> @@ -9,6 +9,8 @@
>>
>> __all__ = [
>> 'ArchiveSubscribersView',
>> + 'PersonalArchiveSubscriptionBreadcrumb',
>> + 'PersonArchiveSubscriptionView',
>> 'PersonArchiveSubscriptionsView',
>> 'traverse_archive_subscription_for_subscriber'
>> ]
>> @@ -34,10 +36,11 @@
>> IArchiveAuthTokenSet)
>> from lp.soyuz.interfaces.archivesubscriber import (
>> IArchiveSubscriberSet, IPersonalArchiveSubscription)
>> +from canonical.launchpad.webapp.breadcrumb import Breadcrumb
>> from canonical.launchpad.webapp.launchpadform import (
>> action, custom_widget, LaunchpadFormView, LaunchpadEditFormView)
>> from canonical.launchpad.webapp.publisher import (
>> - canonical_url, LaunchpadView)
>> + canonical_url, LaunchpadView, Navigation)
>> from canonical.widgets import DateWidget
>> from canonical.widgets.popup import PersonPickerWidget
>>
>> @@ -65,6 +68,20 @@
>> """See `IPersonalArchiveSubscription`."""
>> return "Access to %s" % self.archive.displayname
>>
>> + @property
>> + def title(self):
>
> I know of page_title and label, which are used for this purpose, did you mean
> one of them or am I not aware of this third one?

No, the helper class to which this belongs (PersonalArchiveSubscription)
is used as the context for these views, and so like normal content
classes, it needs a title attribute for when the base template tries to
access context.title.

>
>> + """Required for default headings in templates."""
>> + return self.displayname
>> +
>> +
>> +class PersonalArchiveSubscriptionNavigation(Navigation):
>> + """Navigation for `IPersonalArchiveSubscription`.
>> +
>> + Without this, a breadcrumb will not be created for personal
>> + archive subscription objects.
>
> This is no longer needed, actually -- a fix for the bug (423898) is included
> in my breadcrumbs branch that is on PQM now.

Great, removed.

>
>> + """
>> + usedfor = IPersonalArchiveSubscription
>> +
>> def traverse_archive_subscription_for_subscriber(subscriber, archive_id):
>> """Return the subscription for a subscriber to an archive."""
>> subscription = None
>> @@ -79,6 +96,14 @@
>> return PersonalArchiveSubscription(subscriber, archive)
>>
>>
>> +class PersonalArchiveSubscriptionBreadcrumb(Breadcrumb):
>> + """Builds a breadcrumb for `PersonalArchiveSubscription`."""
>> +
>> + @property
>> + def text(self):
>> + return self.context.displayname
>
> You can use webapp.breadcrumbs.DisplaynameBreadcrumb in the zcml and get rid
> of this adapter altogether. It'd be great if you could do a quick check for
> other places where that generic adapter could replace other speci...

1=== added file 'lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js'
2--- lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js 1970-01-01 00:00:00 +0000
3+++ lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js 2009-09-29 07:26:40 +0000
4@@ -0,0 +1,54 @@
5+/* Copyright 2009 Canonical Ltd. This software is licensed under the
6+ * GNU Affero General Public License version 3 (see the file LICENSE).
7+ *
8+ * Enhancements for adding ppa subscribers.
9+ *
10+ * @module ArchiveSubscribersIndex
11+ * @requires event, node, oop
12+ */
13+YUI.add('soyuz.archivesubscribers_index', function(Y) {
14+
15+var soyuz = Y.namespace('soyuz');
16+
17+/*
18+ * Setup the style and click handler for the add subscriber link.
19+ *
20+ * @method setup_archivesubscribers_index
21+ */
22+Y.soyuz.setup_archivesubscribers_index = function() {
23+ // If there are no errors then we hide the add-subscriber row and
24+ // potentially the whole table if there are no subscribers.
25+ if (Y.Lang.isNull(Y.get('p.error.message'))) {
26+
27+ // Hide the add-subscriber row.
28+ var add_subscriber_row = Y.get(
29+ '#archive-subscribers .add-subscriber');
30+ add_subscriber_row.setStyle('display', 'none');
31+
32+ // If there are no subscribers, then hide the complete section.
33+ var subscribers = Y.get('#subscribers');
34+ if (Y.Lang.isObject(Y.get('#no-subscribers'))) {
35+ subscribers.setStyle('display', 'none');
36+ }
37+ }
38+
39+ // Add a link to open the add-subscriber row.
40+ var placeholder = Y.get('#add-subscriber-placeholder');
41+ placeholder.set(
42+ 'innerHTML',
43+ '<a class="js-action sprite add" href="#">Add access</a>');
44+
45+ // Unfortunately we can't use the lazr slider, as it uses display:block
46+ // which breaks table rows (they use display:table-row).
47+ function show_add_subscriber(e) {
48+ e.preventDefault();
49+ subscribers.setStyle('display', 'block');
50+ add_subscriber_row.setStyle('display', 'table-row');
51+ }
52+
53+ Y.on('click', show_add_subscriber,
54+ '#add-subscriber-placeholder a');
55+};
56+
57+}, '0.1', {requires: ['oop', 'node', 'event']});
58+
59
60=== added file 'lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html'
61--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html 1970-01-01 00:00:00 +0000
62+++ lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html 2009-09-29 09:28:33 +0000
63@@ -0,0 +1,46 @@
64+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
65+<html>
66+ <head>
67+ <title>Launchpad ArchiveSubscriberIndex</title>
68+
69+ <!-- YUI 3.0 Setup -->
70+ <script type="text/javascript" src="../../../icing/yui/current/build/yui/yui.js"></script>
71+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssreset/reset.css"/>
72+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssfonts/fonts.css"/>
73+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssbase/base.css"/>
74+ <link rel="stylesheet" href="../../test.css" />
75+
76+ <!-- The module under test -->
77+ <script type="text/javascript" src="../archivesubscribers_index.js"></script>
78+
79+ <!-- The test suite -->
80+ <script type="text/javascript" src="archivesubscribers_index.js"></script>
81+</head>
82+<body class="yui-skin-sam">
83+ <h1>Testing the ArchiveSubscribersIndex javascript</h1>
84+
85+ <h2>Errors</h2>
86+ <div id="errors">
87+ </div>
88+
89+ <h2>Add subscriber place-holder</h2>
90+ <div id="add-subscriber-placeholder">
91+ </div>
92+
93+ <h2>Subscribers</h2>
94+ <div id="subscribers">
95+ <table id="archive-subscribers">
96+ <thead>
97+ <tr>
98+ <th>Name</th>
99+ <th>Expires</th>
100+ <th colspan="2">Comment</th>
101+ </tr>
102+ </thead>
103+ <tbody>
104+ </tbody>
105+ </table>
106+ </div>
107+ <div id="log"></div>
108+</body>
109+</html>
110
111=== added file 'lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js'
112--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js 1970-01-01 00:00:00 +0000
113+++ lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js 2009-09-29 09:28:33 +0000
114@@ -0,0 +1,134 @@
115+/* Copyright 2009 Canonical Ltd. This software is licensed under the
116+ GNU Affero General Public License version 3 (see the file LICENSE). */
117+
118+YUI({
119+ base: '../../../icing/yui/current/build/',
120+ filter: 'raw',
121+ combine: false
122+ }).use(
123+ 'yuitest', 'console', 'soyuz.archivesubscribers_index', function(Y) {
124+
125+var Assert = Y.Assert; // For easy access to isTrue(), etc.
126+
127+var suite = new Y.Test.Suite("ArchiveSubscriber Tests");
128+
129+suite.add(new Y.Test.Case({
130+
131+ name: 'add-subscriber',
132+
133+ setUp: function() {
134+ this.add_subscriber_placeholder = Y.get(
135+ '#add-subscriber-placeholder');
136+ this.archive_subscribers_table_body = Y.get(
137+ '#archive-subscribers').query('tbody');
138+ this.error_div = Y.get('#errors');
139+ this.subscribers_div = Y.get('#subscribers');
140+
141+
142+ // Ensure there are no errors displayed.
143+ this.error_div.set('innerHTML', '');
144+
145+ // Ensure the add subscriber place-holder is empty.
146+ this.add_subscriber_placeholder.set('innerHTML', '');
147+
148+ // Ensure the table has the correct structure.
149+ this.archive_subscribers_table_body.set(
150+ 'innerHTML', [
151+ '<tr class="add-subscriber">',
152+ '<td>New 1</td>',
153+ '<td>New 2</td>',
154+ '<td>New 3</td>',
155+ '<td>Add</td>',
156+ '</tr>',
157+ '<tr>',
158+ '<td>Existing 1</td>',
159+ '<td>Existing 2</td>',
160+ '<td>Existing 3</td>',
161+ '<td>Edit</td>',
162+ '</tr>'
163+ ].join(''));
164+
165+ this.add_subscriber_row = Y.get(
166+ '#archive-subscribers .add-subscriber');
167+ },
168+
169+ test_add_row_displayed_by_default: function() {
170+ Assert.areEqual(
171+ 'table-row', this.add_subscriber_row.getStyle('display'),
172+ 'The add subscriber row degrades to display without js.');
173+ },
174+
175+ test_subscribers_displayed_by_default: function() {
176+ Assert.areEqual(
177+ 'block', this.subscribers_div.getStyle('display'),
178+ 'The subscribers section is displayed by default without js.');
179+ },
180+
181+ test_add_row_hidden_after_setup: function() {
182+ Y.soyuz.setup_archivesubscribers_index();
183+ Assert.areEqual(
184+ 'none', this.add_subscriber_row.getStyle('display'),
185+ 'The add subscriber row is hidden during setup.');
186+ },
187+
188+ test_subscribers_section_displayed_after_setup: function() {
189+ Y.soyuz.setup_archivesubscribers_index();
190+ Assert.areEqual(
191+ 'block', this.subscribers_div.getStyle('display'),
192+ 'The subscribers div normally remains displayed after setup.');
193+ },
194+
195+ test_subscribers_section_hidden_when_no_subscribers: function() {
196+ // Add a paragraph with the no-subscribers id.
197+ this.error_div.set('innerHTML', '<p id="no-subscribers">blah</p>');
198+ Y.soyuz.setup_archivesubscribers_index();
199+ Assert.areEqual(
200+ 'none', this.subscribers_div.getStyle('display'),
201+ 'The subscribers div is hidden when no subscribers yet.');
202+ },
203+
204+ test_add_row_displayed_when_errors_present: function() {
205+ // Add an error paragraph.
206+ this.error_div.set('innerHTML', '<p class="error message">Blah</p>');
207+ Y.soyuz.setup_archivesubscribers_index();
208+ Assert.areEqual(
209+ 'table-row', this.add_subscriber_row.getStyle('display'),
210+ 'The add subscriber row is displayed if there are errors ' +
211+ 'present.');
212+ },
213+
214+ test_add_access_link_added_after_setup: function() {
215+ Y.soyuz.setup_archivesubscribers_index();
216+ Assert.areEqual(
217+ '<a class="js-action sprite add" href="#">Add access</a>',
218+ this.add_subscriber_placeholder.get('innerHTML'),
219+ "The 'Add access' link is created during setup.")
220+ },
221+
222+ test_click_add_access_displays_add_row: function() {
223+ Y.soyuz.setup_archivesubscribers_index();
224+ var link_node = this.add_subscriber_placeholder.query('a');
225+ Assert.areEqual(
226+ 'Add access', link_node.get('innerHTML'));
227+
228+ Y.Event.simulate(Y.Node.getDOMNode(link_node), 'click');
229+
230+ Assert.areEqual(
231+ 'table-row', this.add_subscriber_row.getStyle('display'),
232+ "The add subscriber row is displayed after clicking " +
233+ "'Add access'");
234+ }
235+}));
236+
237+Y.Test.Runner.add(suite);
238+
239+var yconsole = new Y.Console({
240+ newestOnTop: false
241+});
242+yconsole.render('#log');
243+
244+Y.on('domready', function() {
245+ Y.Test.Runner.run();
246+});
247+
248+});
249
250=== modified file 'lib/lp/soyuz/browser/archivesubscription.py'
251--- lib/lp/soyuz/browser/archivesubscription.py 2009-09-14 08:08:51 +0000
252+++ lib/lp/soyuz/browser/archivesubscription.py 2009-09-29 07:21:40 +0000
253@@ -9,7 +9,6 @@
254
255 __all__ = [
256 'ArchiveSubscribersView',
257- 'PersonalArchiveSubscriptionBreadcrumb',
258 'PersonArchiveSubscriptionView',
259 'PersonArchiveSubscriptionsView',
260 'traverse_archive_subscription_for_subscriber'
261@@ -74,14 +73,6 @@
262 return self.displayname
263
264
265-class PersonalArchiveSubscriptionNavigation(Navigation):
266- """Navigation for `IPersonalArchiveSubscription`.
267-
268- Without this, a breadcrumb will not be created for personal
269- archive subscription objects.
270- """
271- usedfor = IPersonalArchiveSubscription
272-
273 def traverse_archive_subscription_for_subscriber(subscriber, archive_id):
274 """Return the subscription for a subscriber to an archive."""
275 subscription = None
276@@ -96,14 +87,6 @@
277 return PersonalArchiveSubscription(subscriber, archive)
278
279
280-class PersonalArchiveSubscriptionBreadcrumb(Breadcrumb):
281- """Builds a breadcrumb for `PersonalArchiveSubscription`."""
282-
283- @property
284- def text(self):
285- return self.context.displayname
286-
287-
288 class IArchiveSubscriberUI(Interface):
289 """A custom interface for user interaction with archive subscriptions.
290
291
292=== modified file 'lib/lp/soyuz/browser/configure.zcml'
293--- lib/lp/soyuz/browser/configure.zcml 2009-09-28 13:56:17 +0000
294+++ lib/lp/soyuz/browser/configure.zcml 2009-09-29 07:21:40 +0000
295@@ -520,9 +520,6 @@
296 <browser:defaultView
297 for="lp.soyuz.interfaces.archivesubscriber.IPersonalArchiveSubscription"
298 name="+index"/>
299- <browser:navigation
300- module="lp.soyuz.browser.archivesubscription"
301- classes="PersonalArchiveSubscriptionNavigation"/>
302 <browser:defaultView
303 for="lp.soyuz.interfaces.distributionsourcepackagerelease.IDistributionSourcePackageRelease"
304 name="+index"/>
305
306=== modified file 'lib/lp/soyuz/browser/distroarchseriesbinarypackage.py'
307--- lib/lp/soyuz/browser/distroarchseriesbinarypackage.py 2009-09-03 15:17:24 +0000
308+++ lib/lp/soyuz/browser/distroarchseriesbinarypackage.py 2009-09-29 07:21:40 +0000
309@@ -4,7 +4,6 @@
310 __metaclass__ = type
311
312 __all__ = [
313- 'DistroArchSeriesBinaryPackageBreadcrumb',
314 'DistroArchSeriesBinaryPackageNavigation',
315 'DistroArchSeriesBinaryPackageView',
316 ]
317@@ -16,14 +15,6 @@
318 from canonical.lazr.utils import smartquote
319
320
321-class DistroArchSeriesBinaryPackageBreadcrumb(Breadcrumb):
322- """A breadcrumb for `DistroArchSeriesBinaryPackage`."""
323-
324- @property
325- def text(self):
326- return self.context.name
327-
328-
329 class DistroArchSeriesBinaryPackageOverviewMenu(ApplicationMenu):
330
331 usedfor = IDistroArchSeriesBinaryPackage
332
333=== modified file 'lib/lp/soyuz/browser/tests/test_breadcrumbs.py'
334--- lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-11 10:52:54 +0000
335+++ lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-29 07:21:40 +0000
336@@ -76,7 +76,6 @@
337 owner, self.ppa)
338
339 def test_personal_archive_subscription(self):
340-
341 self.traversed_objects = [
342 self.root, self.ppa.owner, self.personal_archive_subscription]
343 subscription_url = canonical_url(self.personal_archive_subscription)
344
345=== modified file 'lib/lp/soyuz/configure.zcml'
346--- lib/lp/soyuz/configure.zcml 2009-09-24 08:17:04 +0000
347+++ lib/lp/soyuz/configure.zcml 2009-09-29 07:21:40 +0000
348@@ -582,7 +582,7 @@
349 <adapter
350 for="lp.soyuz.interfaces.distroarchseriesbinarypackage.IDistroArchSeriesBinaryPackage"
351 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
352- factory="lp.soyuz.browser.distroarchseriesbinarypackage.DistroArchSeriesBinaryPackageBreadcrumb"
353+ factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"
354 permission="zope.Public" />
355
356 <!-- PublishedPackage -->
357@@ -650,7 +650,7 @@
358 <adapter
359 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
360 for="lp.soyuz.interfaces.archivesubscriber.IPersonalArchiveSubscription"
361- factory="lp.soyuz.browser.archivesubscription.PersonalArchiveSubscriptionBreadcrumb"
362+ factory="canonical.launchpad.webapp.breadcrumb.DisplaynameBreadcrumb"
363 permission="zope.Public"/>
364 <subscriber
365 for="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriber
366
367=== modified file 'lib/lp/soyuz/templates/archive-subscribers.pt'
368--- lib/lp/soyuz/templates/archive-subscribers.pt 2009-09-18 08:09:23 +0000
369+++ lib/lp/soyuz/templates/archive-subscribers.pt 2009-09-29 07:21:40 +0000
370@@ -10,6 +10,11 @@
371 <metal:block fill-slot="head_epilogue">
372 <metal:yui-dependencies
373 use-macro="context/@@launchpad_widget_macros/yui2calendar-dependencies" />
374+
375+ <tal:devmode condition="devmode">
376+ <script type="text/javascript"
377+ tal:attributes="src string:${lp_js}/soyuz/archivesubscribers_index.js"></script>
378+ </tal:devmode>
379 </metal:block>
380
381 <div metal:fill-slot="main">
382@@ -92,51 +97,12 @@
383 </table>
384 </form>
385 </div><!-- class="portlet" -->
386+ <script type="text/javascript" id="setup-archivesubscribers-index">
387+ YUI().use('soyuz.archivesubscribers_index', function(Y) {
388+ Y.soyuz.setup_archivesubscribers_index();
389+ });
390+ </script>
391 </div>
392- <script type="text/javascript">
393-YUI({
394- base: '../../lib/yui/current/build/',
395- filter: 'raw'
396- }).use('node', 'event', 'lazr.effects', function(Y) {
397-
398- // If there are no errors then we hide the add-subscriber row and
399- // potentially the whole table if there are no subscribers.
400- if (Y.Lang.isNull(Y.get('p.error.message'))) {
401-
402- // Hide the add-subscriber row.
403- var add_subscriber_row = Y.get(
404- '#archive-subscribers .add-subscriber');
405- add_subscriber_row.setStyle('display', 'none');
406-
407- // If there are no subscribers, then hide the complete section.
408- var subscribers = Y.get('#subscribers');
409- if (Y.Lang.isObject(Y.get('#no-subscribers'))) {
410- subscribers.setStyle('display', 'none');
411- }
412- }
413-
414- // Add a link to open the add-subscriber row.
415- var placeholder = Y.get('#add-subscriber-placeholder');
416- placeholder.set(
417- 'innerHTML', '<a class="js-action sprite add" href="#" />');
418- var add_subscriber_node = Y.get("#add-subscriber-placeholder a");
419- add_subscriber_node.set('innerHTML', 'Add access');
420-
421- // Unfortunately we can't use the lazr slider, as it uses display:block
422- // which breaks table rows (they use display:table-row).
423- function show_add_subscriber(e) {
424- e.preventDefault();
425- subscribers.setStyle('display', 'block');
426- add_subscriber_row.setStyle('display', 'table-row');
427- }
428-
429- Y.on('click', show_add_subscriber,
430- '#add-subscriber-placeholder a');
431-
432-});
433- </script>
434-
435-
436 </div>
437 </body>
438 </html>
439
440=== added directory 'lib/lp/soyuz/windmill'
441=== added file 'lib/lp/soyuz/windmill/__init__.py'
442=== added file 'lib/lp/soyuz/windmill/testing.py'
443--- lib/lp/soyuz/windmill/testing.py 1970-01-01 00:00:00 +0000
444+++ lib/lp/soyuz/windmill/testing.py 2009-09-29 12:09:39 +0000
445@@ -0,0 +1,18 @@
446+# Copyright 2009 Canonical Ltd. This software is licensed under the
447+# GNU Affero General Public License version 3 (see the file LICENSE).
448+
449+"""Soyuz-specific testing infrastructure for Windmill."""
450+
451+__metaclass__ = type
452+__all__ = [
453+ 'SoyuzWindmillLayer',
454+ ]
455+
456+
457+from canonical.testing.layers import BaseWindmillLayer
458+
459+
460+class SoyuzWindmillLayer(BaseWindmillLayer):
461+ """Layer for Soyuz Windmill tests."""
462+
463+ base_url = 'http://launchpad.dev:8085/'
464
465=== added directory 'lib/lp/soyuz/windmill/tests'
466=== added file 'lib/lp/soyuz/windmill/tests/__init__.py'
467=== added file 'lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py'
468--- lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py 1970-01-01 00:00:00 +0000
469+++ lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py 2009-09-29 12:09:39 +0000
470@@ -0,0 +1,96 @@
471+# Copyright 2009 Canonical Ltd. This software is licensed under the
472+# GNU Affero General Public License version 3 (see the file LICENSE).
473+
474+"""Test for the archive subscribers index page.."""
475+
476+__metaclass__ = type
477+__all__ = []
478+
479+import transaction
480+import unittest
481+
482+from windmill.authoring import WindmillTestClient
483+
484+from zope.component import getUtility
485+
486+from canonical.launchpad.ftests import login, logout
487+from canonical.launchpad.windmill.testing.lpuser import LaunchpadUser
488+from lp.registry.interfaces.distribution import IDistributionSet
489+from lp.soyuz.windmill.testing import SoyuzWindmillLayer
490+from lp.testing import TestCaseWithFactory
491+
492+WAIT_PAGELOAD = u'30000'
493+WAIT_ELEMENT_COMPLETE = u'30000'
494+WAIT_CHECK_CHANGE = u'1000'
495+ADD_ACCESS_LINK = u'//a[@class="js-action sprite add"]'
496+CHOOSE_SUBSCRIBER_LINK = u'//a[@id="show-widget-field-subscriber"]'
497+SUBSCRIBER_SEARCH_FIELD = (
498+ u'//div[@id="yui-pretty-overlay-modal"]//input[@name="search"]')
499+SUBSCRIBER_SEARCH_BUTTON = u'//div[@id="yui-pretty-overlay-modal"]//button'
500+FIRST_SUBSCRIBER_RESULT = (
501+ u'//div[@id="yui-pretty-overlay-modal"]'
502+ '//span[@class="yui-picker-result-title"]')
503+
504+
505+class TestArchiveSubscribersIndex(TestCaseWithFactory):
506+
507+ layer = SoyuzWindmillLayer
508+
509+ def setUp(self):
510+ """Create a private PPA."""
511+ super(TestArchiveSubscribersIndex, self).setUp()
512+
513+ user = self.factory.makePerson(
514+ name='joe-bloggs', email='joe@example.com', password='joe',
515+ displayname='Joe Bloggs')
516+ ubuntu = getUtility(IDistributionSet)['ubuntu']
517+ self.ppa = self.factory.makeArchive(
518+ owner=user, name='myppa', distribution=ubuntu)
519+
520+ login('foo.bar@canonical.com')
521+ self.ppa.private = True
522+ self.ppa.buildd_secret = 'secret'
523+ logout()
524+ transaction.commit()
525+
526+ self.lpuser = LaunchpadUser(
527+ 'Joe Bloggs', 'joe@example.com', 'joe')
528+
529+ def test_add_subscriber(self):
530+ """Test adding a private PPA subscriber.."""
531+ client = WindmillTestClient('Adding private PPA subscribers.')
532+
533+ self.lpuser.ensure_login(client)
534+
535+ client.open(url='http://launchpad.dev:8085/~joe-bloggs/'
536+ '+archive/myppa/+subscriptions')
537+ client.waits.forPageLoad(timeout=WAIT_PAGELOAD)
538+
539+ # Click on the JS add access action.
540+ client.waits.forElement(xpath=ADD_ACCESS_LINK)
541+ client.click(xpath=ADD_ACCESS_LINK)
542+
543+ # Open the picker, search for 'launchpad' and choose the first
544+ # result
545+ client.click(xpath=CHOOSE_SUBSCRIBER_LINK)
546+ client.type(xpath=SUBSCRIBER_SEARCH_FIELD, text='launchpad')
547+ client.click(xpath=SUBSCRIBER_SEARCH_BUTTON)
548+
549+ client.waits.forElement(xpath=FIRST_SUBSCRIBER_RESULT)
550+ client.click(xpath=FIRST_SUBSCRIBER_RESULT)
551+
552+ # Add the new subscriber.
553+ client.click(id='field.actions.add')
554+ client.waits.forPageLoad(timeout=WAIT_PAGELOAD)
555+
556+ # And verify that the correct informational message is displayed.
557+ # It would be nice if we could use ... here.
558+ client.asserts.assertText(
559+ xpath=u'//div[@class="informational message"]',
560+ validator='You have granted access for Launchpad Developers '
561+ 'to install software from PPA named myppa for Joe '
562+ 'Bloggs. Members of Launchpad Developers will be '
563+ 'notified of the access via email.')
564+
565+def test_suite():
566+ return unittest.TestLoader().loadTestsFromName(__name__)
Revision history for this message
Graham Binns (gmb) wrote :

Approved with the changes requested in IRC w.r.t comments in the tests.

review: Approve (js)

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'
2--- lib/canonical/launchpad/icing/style-3-0.css 2009-09-23 10:29:52 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css 2009-09-30 14:18:17 +0000
4@@ -366,6 +366,9 @@
5 vertical-align: bottom;
6 }
7
8+form {
9+ margin-bottom: 1em;
10+}
11 form h1 {
12 margin-bottom: 1em;
13 }
14
15=== added file 'lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js'
16--- lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js 1970-01-01 00:00:00 +0000
17+++ lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js 2009-09-30 14:18:17 +0000
18@@ -0,0 +1,54 @@
19+/* Copyright 2009 Canonical Ltd. This software is licensed under the
20+ * GNU Affero General Public License version 3 (see the file LICENSE).
21+ *
22+ * Enhancements for adding ppa subscribers.
23+ *
24+ * @module ArchiveSubscribersIndex
25+ * @requires event, node, oop
26+ */
27+YUI.add('soyuz.archivesubscribers_index', function(Y) {
28+
29+var soyuz = Y.namespace('soyuz');
30+
31+/*
32+ * Setup the style and click handler for the add subscriber link.
33+ *
34+ * @method setup_archivesubscribers_index
35+ */
36+Y.soyuz.setup_archivesubscribers_index = function() {
37+ // If there are no errors then we hide the add-subscriber row and
38+ // potentially the whole table if there are no subscribers.
39+ if (Y.Lang.isNull(Y.get('p.error.message'))) {
40+
41+ // Hide the add-subscriber row.
42+ var add_subscriber_row = Y.get(
43+ '#archive-subscribers .add-subscriber');
44+ add_subscriber_row.setStyle('display', 'none');
45+
46+ // If there are no subscribers, then hide the complete section.
47+ var subscribers = Y.get('#subscribers');
48+ if (Y.Lang.isObject(Y.get('#no-subscribers'))) {
49+ subscribers.setStyle('display', 'none');
50+ }
51+ }
52+
53+ // Add a link to open the add-subscriber row.
54+ var placeholder = Y.get('#add-subscriber-placeholder');
55+ placeholder.set(
56+ 'innerHTML',
57+ '<a class="js-action sprite add" href="#">Add access</a>');
58+
59+ // Unfortunately we can't use the lazr slider, as it uses display:block
60+ // which breaks table rows (they use display:table-row).
61+ function show_add_subscriber(e) {
62+ e.preventDefault();
63+ subscribers.setStyle('display', 'block');
64+ add_subscriber_row.setStyle('display', 'table-row');
65+ }
66+
67+ Y.on('click', show_add_subscriber,
68+ '#add-subscriber-placeholder a');
69+};
70+
71+}, '0.1', {requires: ['oop', 'node', 'event']});
72+
73
74=== added file 'lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html'
75--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html 1970-01-01 00:00:00 +0000
76+++ lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html 2009-09-30 14:18:17 +0000
77@@ -0,0 +1,46 @@
78+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
79+<html>
80+ <head>
81+ <title>Launchpad ArchiveSubscriberIndex</title>
82+
83+ <!-- YUI 3.0 Setup -->
84+ <script type="text/javascript" src="../../../icing/yui/current/build/yui/yui.js"></script>
85+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssreset/reset.css"/>
86+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssfonts/fonts.css"/>
87+ <link rel="stylesheet" href="../../../icing/yui/current/build/cssbase/base.css"/>
88+ <link rel="stylesheet" href="../../test.css" />
89+
90+ <!-- The module under test -->
91+ <script type="text/javascript" src="../archivesubscribers_index.js"></script>
92+
93+ <!-- The test suite -->
94+ <script type="text/javascript" src="archivesubscribers_index.js"></script>
95+</head>
96+<body class="yui-skin-sam">
97+ <h1>Testing the ArchiveSubscribersIndex javascript</h1>
98+
99+ <h2>Errors</h2>
100+ <div id="errors">
101+ </div>
102+
103+ <h2>Add subscriber place-holder</h2>
104+ <div id="add-subscriber-placeholder">
105+ </div>
106+
107+ <h2>Subscribers</h2>
108+ <div id="subscribers">
109+ <table id="archive-subscribers">
110+ <thead>
111+ <tr>
112+ <th>Name</th>
113+ <th>Expires</th>
114+ <th colspan="2">Comment</th>
115+ </tr>
116+ </thead>
117+ <tbody>
118+ </tbody>
119+ </table>
120+ </div>
121+ <div id="log"></div>
122+</body>
123+</html>
124
125=== added file 'lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js'
126--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js 1970-01-01 00:00:00 +0000
127+++ lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js 2009-09-30 14:18:17 +0000
128@@ -0,0 +1,147 @@
129+/* Copyright 2009 Canonical Ltd. This software is licensed under the
130+ GNU Affero General Public License version 3 (see the file LICENSE). */
131+
132+YUI({
133+ base: '../../../icing/yui/current/build/',
134+ filter: 'raw',
135+ combine: false
136+ }).use(
137+ 'yuitest', 'console', 'soyuz.archivesubscribers_index', function(Y) {
138+
139+var Assert = Y.Assert; // For easy access to isTrue(), etc.
140+
141+var suite = new Y.Test.Suite("ArchiveSubscriber Tests");
142+
143+suite.add(new Y.Test.Case({
144+
145+ name: 'add-subscriber',
146+
147+ setUp: function() {
148+ this.add_subscriber_placeholder = Y.get(
149+ '#add-subscriber-placeholder');
150+ this.archive_subscribers_table_body = Y.get(
151+ '#archive-subscribers').query('tbody');
152+ this.error_div = Y.get('#errors');
153+ this.subscribers_div = Y.get('#subscribers');
154+
155+
156+ // Ensure there are no errors displayed.
157+ this.error_div.set('innerHTML', '');
158+
159+ // Ensure the add subscriber place-holder is empty.
160+ this.add_subscriber_placeholder.set('innerHTML', '');
161+
162+ // Ensure the table has the correct structure.
163+ this.archive_subscribers_table_body.set(
164+ 'innerHTML', [
165+ '<tr class="add-subscriber">',
166+ '<td>New 1</td>',
167+ '<td>New 2</td>',
168+ '<td>New 3</td>',
169+ '<td>Add</td>',
170+ '</tr>',
171+ '<tr>',
172+ '<td>Existing 1</td>',
173+ '<td>Existing 2</td>',
174+ '<td>Existing 3</td>',
175+ '<td>Edit</td>',
176+ '</tr>'
177+ ].join(''));
178+
179+ this.add_subscriber_row = Y.get(
180+ '#archive-subscribers .add-subscriber');
181+ },
182+
183+ test_add_row_displayed_by_default: function() {
184+ // The add subscriber row is displayed when the JS is not run.
185+ Assert.areEqual(
186+ 'table-row', this.add_subscriber_row.getStyle('display'),
187+ 'The add subscriber row should display when the js is not run.');
188+ },
189+
190+ test_subscribers_displayed_by_default: function() {
191+ // The subscribers section is displayed when the js is not run.
192+ Assert.areEqual(
193+ 'block', this.subscribers_div.getStyle('display'),
194+ 'The subscribers section should display without js.');
195+ },
196+
197+ test_add_row_hidden_after_setup: function() {
198+ // The add subscriber row is hidden during setup.
199+ Y.soyuz.setup_archivesubscribers_index();
200+ Assert.areEqual(
201+ 'none', this.add_subscriber_row.getStyle('display'),
202+ 'The add subscriber row should be hidden during setup.');
203+ },
204+
205+ test_subscribers_section_displayed_after_setup: function() {
206+ // The subscribers div normally remains displayed after setup.
207+ Y.soyuz.setup_archivesubscribers_index();
208+ Assert.areEqual(
209+ 'block', this.subscribers_div.getStyle('display'),
210+ 'The subscribers div should remain displayed after setup.');
211+ },
212+
213+ test_subscribers_section_hidden_when_no_subscribers: function() {
214+ // The subscribers div is hidden when there are no subscribers.
215+
216+ // Add a paragraph with the no-subscribers id.
217+ this.error_div.set('innerHTML', '<p id="no-subscribers">blah</p>');
218+ Y.soyuz.setup_archivesubscribers_index();
219+ Assert.areEqual(
220+ 'none', this.subscribers_div.getStyle('display'),
221+ 'The subscribers div should be hidden when there are ' +
222+ 'no subscribers.');
223+ },
224+
225+ test_add_row_displayed_when_errors_present: function() {
226+ // The add subscriber row is not hidden if there are validation
227+ // errors.
228+
229+ // Add an error paragraph.
230+ this.error_div.set('innerHTML', '<p class="error message">Blah</p>');
231+ Y.soyuz.setup_archivesubscribers_index();
232+ Assert.areEqual(
233+ 'table-row', this.add_subscriber_row.getStyle('display'),
234+ 'The add subscriber row should not be hidden if there are ' +
235+ 'errors present.');
236+ },
237+
238+ test_add_access_link_added_after_setup: function() {
239+ // The 'Add access' link is created during setup.
240+
241+ Y.soyuz.setup_archivesubscribers_index();
242+ Assert.areEqual(
243+ '<a class="js-action sprite add" href="#">Add access</a>',
244+ this.add_subscriber_placeholder.get('innerHTML'),
245+ "The 'Add access' link should be created during setup.")
246+ },
247+
248+ test_click_add_access_displays_add_row: function() {
249+ // The add subscriber row is displayed after clicking 'Add access'.
250+ Y.soyuz.setup_archivesubscribers_index();
251+ var link_node = this.add_subscriber_placeholder.query('a');
252+ Assert.areEqual(
253+ 'Add access', link_node.get('innerHTML'));
254+
255+ Y.Event.simulate(Y.Node.getDOMNode(link_node), 'click');
256+
257+ Assert.areEqual(
258+ 'table-row', this.add_subscriber_row.getStyle('display'),
259+ "The add subscriber row should be displayed after clicking " +
260+ "'Add access'");
261+ }
262+}));
263+
264+Y.Test.Runner.add(suite);
265+
266+var yconsole = new Y.Console({
267+ newestOnTop: false
268+});
269+yconsole.render('#log');
270+
271+Y.on('domready', function() {
272+ Y.Test.Runner.run();
273+});
274+
275+});
276
277=== modified file 'lib/canonical/launchpad/pagetitles.py'
278--- lib/canonical/launchpad/pagetitles.py 2009-09-23 14:58:12 +0000
279+++ lib/canonical/launchpad/pagetitles.py 2009-09-30 14:18:17 +0000
280@@ -128,10 +128,6 @@
281
282 archive_edit_dependencies = ContextDisplayName('Edit dependencies for %s')
283
284-archive_subscriber_edit = ContextDisplayName('Edit %s')
285-
286-archive_subscribers = ContextDisplayName('Manage access to %s')
287-
288 bazaar_all_branches = 'All branches in the Launchpad Bazaar'
289
290 bazaar_index = 'Launchpad Branches'
291@@ -562,10 +558,6 @@
292
293 people_requestmerge_multiple = 'Merge Launchpad accounts'
294
295-person_archive_subscription = ContextDisplayName('%s')
296-
297-person_archive_subscriptions = 'Private PPA access'
298-
299 person_answer_contact_for = ContextDisplayName(
300 'Projects for which %s is an answer contact')
301
302
303=== modified file 'lib/lp/soyuz/browser/archivesubscription.py'
304--- lib/lp/soyuz/browser/archivesubscription.py 2009-08-12 08:40:41 +0000
305+++ lib/lp/soyuz/browser/archivesubscription.py 2009-09-30 14:18:17 +0000
306@@ -9,6 +9,7 @@
307
308 __all__ = [
309 'ArchiveSubscribersView',
310+ 'PersonArchiveSubscriptionView',
311 'PersonArchiveSubscriptionsView',
312 'traverse_archive_subscription_for_subscriber'
313 ]
314@@ -34,10 +35,11 @@
315 IArchiveAuthTokenSet)
316 from lp.soyuz.interfaces.archivesubscriber import (
317 IArchiveSubscriberSet, IPersonalArchiveSubscription)
318+from canonical.launchpad.webapp.breadcrumb import Breadcrumb
319 from canonical.launchpad.webapp.launchpadform import (
320 action, custom_widget, LaunchpadFormView, LaunchpadEditFormView)
321 from canonical.launchpad.webapp.publisher import (
322- canonical_url, LaunchpadView)
323+ canonical_url, LaunchpadView, Navigation)
324 from canonical.widgets import DateWidget
325 from canonical.widgets.popup import PersonPickerWidget
326
327@@ -65,6 +67,12 @@
328 """See `IPersonalArchiveSubscription`."""
329 return "Access to %s" % self.archive.displayname
330
331+ @property
332+ def title(self):
333+ """Required for default headings in templates."""
334+ return self.displayname
335+
336+
337 def traverse_archive_subscription_for_subscriber(subscriber, archive_id):
338 """Return the subscription for a subscriber to an archive."""
339 subscription = None
340@@ -111,6 +119,11 @@
341 custom_widget('subscriber', PersonPickerWidget,
342 header="Select the subscriber")
343
344+ @property
345+ def label(self):
346+ """Return a label for the view's main heading."""
347+ return "Manage access to " + self.context.title
348+
349 def initialize(self):
350 """Ensure that we are dealing with a private archive."""
351 # If this archive is not private, then we should not be
352@@ -210,6 +223,11 @@
353 custom_widget('description', TextWidget, displayWidth=40)
354 custom_widget('date_expires', CustomWidgetFactory(DateWidget))
355
356+ @property
357+ def label(self):
358+ """Return a label for the view's main heading."""
359+ return "Edit " + self.context.displayname
360+
361 def validate_update_subscription(self, action, data):
362 """Ensure that the date of expiry is not in the past."""
363 form.getWidgetsData(self.widgets, 'field', data)
364@@ -241,12 +259,12 @@
365 self.context.subscriber.displayname)
366 self.request.response.addNotification(notification)
367
368- @action(u'Cancel access', name='cancel')
369+ @action(u'Revoke access', name='cancel')
370 def cancel_subscription(self, action, data):
371 """Cancel the context subscription."""
372 self.context.cancel(self.user)
373
374- notification = "You have cancelled %s's subscription to %s." % (
375+ notification = "You have revoked %s's access to %s." % (
376 self.context.subscriber.displayname,
377 self.context.archive.displayname)
378 self.request.response.addNotification(notification)
379@@ -265,6 +283,8 @@
380 class PersonArchiveSubscriptionsView(LaunchpadView):
381 """A view for displaying a persons archive subscriptions."""
382
383+ label = "Private PPA access"
384+
385 @cachedproperty
386 def subscriptions_with_tokens(self):
387 """Return all the persons archive subscriptions with the token
388@@ -294,6 +314,11 @@
389 tokens.
390 """
391
392+ @property
393+ def label(self):
394+ """Return the label for the view's main heading."""
395+ return self.context.title
396+
397 def initialize(self):
398 """Process any posted actions."""
399 super(PersonArchiveSubscriptionView, self).initialize()
400
401=== modified file 'lib/lp/soyuz/browser/distroarchseriesbinarypackage.py'
402--- lib/lp/soyuz/browser/distroarchseriesbinarypackage.py 2009-09-03 15:17:24 +0000
403+++ lib/lp/soyuz/browser/distroarchseriesbinarypackage.py 2009-09-30 14:18:17 +0000
404@@ -4,7 +4,6 @@
405 __metaclass__ = type
406
407 __all__ = [
408- 'DistroArchSeriesBinaryPackageBreadcrumb',
409 'DistroArchSeriesBinaryPackageNavigation',
410 'DistroArchSeriesBinaryPackageView',
411 ]
412@@ -16,14 +15,6 @@
413 from canonical.lazr.utils import smartquote
414
415
416-class DistroArchSeriesBinaryPackageBreadcrumb(Breadcrumb):
417- """A breadcrumb for `DistroArchSeriesBinaryPackage`."""
418-
419- @property
420- def text(self):
421- return self.context.name
422-
423-
424 class DistroArchSeriesBinaryPackageOverviewMenu(ApplicationMenu):
425
426 usedfor = IDistroArchSeriesBinaryPackage
427
428=== modified file 'lib/lp/soyuz/browser/tests/archivesubscription-views.txt'
429--- lib/lp/soyuz/browser/tests/archivesubscription-views.txt 2009-08-13 19:03:36 +0000
430+++ lib/lp/soyuz/browser/tests/archivesubscription-views.txt 2009-09-30 14:18:17 +0000
431@@ -30,11 +30,16 @@
432 >>> transaction.commit()
433 >>> logout()
434
435+The view includes a label property.
436+
437+ >>> login('celso.providelo@canonical.com')
438+ >>> view = create_initialized_view(cprov.archive, name="+subscriptions")
439+ >>> print view.label
440+ Manage access to PPA for Celso Providelo
441+
442 Initially the view does not display any subscribers, as can be seen
443 using the has_subscriptions property:
444
445- >>> login('celso.providelo@canonical.com')
446- >>> view = create_initialized_view(cprov.archive, name="+subscriptions")
447 >>> view.has_subscriptions
448 False
449
450@@ -166,8 +171,8 @@
451
452 == ArchiveSubscriptionEditView ==
453
454-The ArchiveSubscriptionEditView presents the expiry and description ready
455-for editing, together with Update and Cancel actions:
456+The ArchiveSubsriptionEditView includes a view label for the views main
457+title.
458
459 >>> login('celso.providelo@canonical.com')
460 >>> from lp.soyuz.interfaces.archivesubscriber import (
461@@ -175,12 +180,18 @@
462 >>> spiv_subscription = getUtility(IArchiveSubscriberSet).getByArchive(
463 ... cprov.archive).one()
464 >>> view = create_initialized_view(spiv_subscription, name="+edit")
465+ >>> print view.label
466+ Edit Andrew Bennetts's access to PPA for Celso Providelo
467+
468+The ArchiveSubscriptionEditView presents the expiry and description ready
469+for editing, together with Update and Cancel actions:
470+
471 >>> view.field_names
472 ['date_expires', 'description']
473 >>> for action in view.actions:
474 ... print action.label
475 Save
476- Cancel access
477+ Revoke access
478
479 The ArchiveSubscriptionEditView has a next_url helper property.
480
481@@ -241,7 +252,7 @@
482
483 >>> for notification in view.request.notifications:
484 ... print notification.message
485- You have cancelled Andrew Bennetts's subscription to PPA for
486+ You have revoked Andrew Bennetts's access to PPA for
487 Celso Providelo.
488
489 Just uncancel the subscription before continuing on.
490@@ -306,13 +317,18 @@
491 This view displays a single subscription of a person, as well as the
492 corresponding token information.
493
494-Initially the subscription does not have an active token:
495+The view includes a label to denifen its main heading.
496
497 >>> from lp.soyuz.browser.archivesubscription import (
498 ... PersonalArchiveSubscription)
499 >>> spiv_subscription = PersonalArchiveSubscription(
500 ... spiv_subscription.subscriber, spiv_subscription.archive)
501 >>> view = create_initialized_view(spiv_subscription, name="+index")
502+ >>> print view.label
503+ Access to PPA for Celso Providelo
504+
505+Initially the subscription does not have an active token:
506+
507 >>> print view.active_token
508 None
509
510
511=== modified file 'lib/lp/soyuz/browser/tests/test_breadcrumbs.py'
512--- lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-02 16:32:06 +0000
513+++ lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-30 14:18:17 +0000
514@@ -11,6 +11,8 @@
515 from canonical.launchpad.webapp.tests.breadcrumbs import (
516 BaseBreadcrumbTestCase)
517 from lp.registry.interfaces.distribution import IDistributionSet
518+from lp.soyuz.browser.archivesubscription import PersonalArchiveSubscription
519+from lp.testing import login, login_person
520
521
522 class TestDistroArchSeriesBreadcrumb(BaseBreadcrumbTestCase):
523@@ -55,5 +57,37 @@
524 self.assertEquals(texts[-1], "0.1-1")
525
526
527+class TestArchiveSubscriptionBreadcrumb(BaseBreadcrumbTestCase):
528+
529+ def setUp(self):
530+ super(TestArchiveSubscriptionBreadcrumb, self).setUp()
531+
532+ # Create a private ppa
533+ self.ppa = self.factory.makeArchive()
534+ login('foo.bar@canonical.com')
535+ self.ppa.private = True
536+ self.ppa.buildd_secret = 'secret'
537+
538+ owner = self.ppa.owner
539+ login_person(owner)
540+ self.ppa_subscription = self.ppa.newSubscription(owner, owner)
541+ self.ppa_token = self.ppa.newAuthToken(owner)
542+ self.personal_archive_subscription = PersonalArchiveSubscription(
543+ owner, self.ppa)
544+
545+ def test_personal_archive_subscription(self):
546+ self.traversed_objects = [
547+ self.root, self.ppa.owner, self.personal_archive_subscription]
548+ subscription_url = canonical_url(self.personal_archive_subscription)
549+
550+ urls = self._getBreadcrumbsURLs(
551+ subscription_url, self.traversed_objects)
552+ texts = self._getBreadcrumbsTexts(
553+ subscription_url, self.traversed_objects)
554+
555+ self.assertEquals(subscription_url, urls[-1])
556+ self.assertEquals(
557+ "Access to %s" % self.ppa.displayname, texts[-1])
558+
559 def test_suite():
560 return unittest.TestLoader().loadTestsFromName(__name__)
561
562=== modified file 'lib/lp/soyuz/configure.zcml'
563--- lib/lp/soyuz/configure.zcml 2009-09-04 18:18:39 +0000
564+++ lib/lp/soyuz/configure.zcml 2009-09-30 14:18:17 +0000
565@@ -582,7 +582,7 @@
566 <adapter
567 for="lp.soyuz.interfaces.distroarchseriesbinarypackage.IDistroArchSeriesBinaryPackage"
568 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
569- factory="lp.soyuz.browser.distroarchseriesbinarypackage.DistroArchSeriesBinaryPackageBreadcrumb"
570+ factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"
571 permission="zope.Public" />
572
573 <!-- PublishedPackage -->
574@@ -647,6 +647,11 @@
575 for="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriber"
576 provides="lp.soyuz.browser.archivesubscription.IArchiveSubscriberUI"
577 factory="lp.soyuz.browser.archivesubscription.archive_subscription_ui_adapter"/>
578+ <adapter
579+ provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
580+ for="lp.soyuz.interfaces.archivesubscriber.IPersonalArchiveSubscription"
581+ factory="canonical.launchpad.webapp.breadcrumb.DisplaynameBreadcrumb"
582+ permission="zope.Public"/>
583 <subscriber
584 for="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriber
585 lazr.lifecycle.interfaces.IObjectCreatedEvent"
586
587=== modified file 'lib/lp/soyuz/interfaces/archivesubscriber.py'
588--- lib/lp/soyuz/interfaces/archivesubscriber.py 2009-07-17 00:26:05 +0000
589+++ lib/lp/soyuz/interfaces/archivesubscriber.py 2009-09-30 14:18:17 +0000
590@@ -185,3 +185,6 @@
591
592 displayname = TextLine(title=_("Subscription displayname"),
593 required=False)
594+
595+ title = TextLine(title=_("Subscription title"),
596+ required=False)
597
598=== modified file 'lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt'
599--- lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt 2009-08-13 19:03:36 +0000
600+++ lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt 2009-09-30 14:18:17 +0000
601@@ -176,7 +176,7 @@
602
603 >>> for msg in get_feedback_messages(cprov_browser.contents):
604 ... print msg
605- You have cancelled Launchpad Developers's subscription to PPA
606+ You have revoked Launchpad Developers's access to PPA
607 for Celso Providelo.
608
609
610@@ -242,10 +242,10 @@
611 >>> regeneration_info = find_tag_by_id(
612 ... joe_browser.contents, 'regenerate_token')
613 >>> print(extract_text(regeneration_info))
614- Password security
615+ Reset password
616 If you believe...
617+ Reset password
618 Note: after ...
619- Generate new password
620
621 When Joe clicks on the 'Generate new personal subscription' link then
622 the page is redisplayed with new sources.list entries and a notification.
623
624=== modified file 'lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt'
625--- lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt 2009-08-13 19:03:36 +0000
626+++ lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt 2009-09-30 14:18:17 +0000
627@@ -54,6 +54,7 @@
628 ...
629 http://launchpad.dev/+icing/.../build/lp/calendar.js
630 http://launchpad.dev/+icing/.../yui_2.7.0b/build/calendar/assets/skins/sam/calendar.css
631+ ...
632
633 Initially there are no subscriptions for a newly privatized PPA (although,
634 this may need to change, to add the owner/team). A heading is displayed
635
636=== modified file 'lib/lp/soyuz/templates/archive-subscriber-edit.pt'
637--- lib/lp/soyuz/templates/archive-subscriber-edit.pt 2009-08-05 10:02:22 +0000
638+++ lib/lp/soyuz/templates/archive-subscriber-edit.pt 2009-09-30 14:18:17 +0000
639@@ -12,12 +12,6 @@
640 use-macro="context/@@launchpad_widget_macros/yui2calendar-dependencies" />
641 </metal:block>
642
643- <div metal:fill-slot="heading">
644- <h1 tal:content="CONTEXTS/fmt:pagetitle">
645- Edit access for Joe Smith
646- </h1>
647- </div>
648-
649 <div metal:fill-slot="main">
650 <div class="top-portlet">
651 <p>You can update the expiry or description for the granted access,
652
653=== modified file 'lib/lp/soyuz/templates/archive-subscribers.pt'
654--- lib/lp/soyuz/templates/archive-subscribers.pt 2009-08-05 10:02:22 +0000
655+++ lib/lp/soyuz/templates/archive-subscribers.pt 2009-09-30 14:18:17 +0000
656@@ -10,24 +10,26 @@
657 <metal:block fill-slot="head_epilogue">
658 <metal:yui-dependencies
659 use-macro="context/@@launchpad_widget_macros/yui2calendar-dependencies" />
660+
661+ <tal:devmode condition="devmode">
662+ <script type="text/javascript"
663+ tal:attributes="src string:${lp_js}/soyuz/archivesubscribers_index.js"></script>
664+ </tal:devmode>
665 </metal:block>
666
667- <div metal:fill-slot="heading">
668- <h1 tal:content="CONTEXTS/fmt:pagetitle">
669- Manage access to Blah PPA
670- </h1>
671- </div>
672 <div metal:fill-slot="main">
673 <div class="top-portlet">
674 <p>You can grant access to people or teams to install software
675 from your PPA.
676 </p>
677
678- <div class="subscribers">
679- <p tal:condition="not: view/has_subscriptions" id="no-subscribers">
680- No one has access to install software from this PPA.
681- </p>
682-
683+ <p tal:condition="not: view/has_subscriptions" id="no-subscribers">
684+ No one has access to install software from this PPA.
685+ </p>
686+
687+ <div id="add-subscriber-placeholder"></div>
688+
689+ <div id="subscribers">
690 <p class="error message" tal:condition="view/errors"
691 tal:content="view/error_count" />
692
693@@ -44,13 +46,13 @@
694 id="archive-subscribers" class="listing">
695 <thead>
696 <tr class="archive_subscriber_row">
697- <th>Name</th>
698+ <th style="width:30%">Name</th>
699 <th>Expires</th>
700 <th colspan="2">Comment</th>
701 </tr>
702 </thead>
703 <tbody>
704- <tr>
705+ <tr class="add-subscriber" style="background-color:#eeeeff;">
706 <tal:single-row-form define="widgets widgets|view/widgets"
707 repeat="widget widgets">
708
709@@ -95,8 +97,12 @@
710 </table>
711 </form>
712 </div><!-- class="portlet" -->
713+ <script type="text/javascript" id="setup-archivesubscribers-index">
714+ YUI().use('soyuz.archivesubscribers_index', function(Y) {
715+ Y.soyuz.setup_archivesubscribers_index();
716+ });
717+ </script>
718 </div>
719-
720 </div>
721 </body>
722 </html>
723
724=== modified file 'lib/lp/soyuz/templates/person-archive-subscription.pt'
725--- lib/lp/soyuz/templates/person-archive-subscription.pt 2009-08-05 10:02:22 +0000
726+++ lib/lp/soyuz/templates/person-archive-subscription.pt 2009-09-30 14:18:17 +0000
727@@ -8,12 +8,6 @@
728 >
729 <body>
730
731- <div metal:fill-slot="heading">
732- <h1 tal:content="CONTEXTS/fmt:pagetitle">
733- Access to Joe Smith's private archive
734- </h1>
735- </div>
736-
737 <div metal:fill-slot="main">
738
739 <tal:activate condition="not: view/active_token">
740@@ -54,22 +48,23 @@
741 </div> <!-- signing-key -->
742 </div>
743 <div id="regenerate_token" class="portlet" style="clear:both">
744- <h2>Password security</h2>
745+ <h2>Reset password</h2>
746 <p>If you believe the security of your password for this access
747- has been compromised, you can generate a new one. After you've
748+ has been compromised, you reset your password. After you've
749 requested a new password, you'll see new "sources.list" entries
750 on this page. You'll need to update them on your computer.
751 </p>
752- <p>Note: after requesting a new password for your personal
753- access &mdash; and updating your sources.list with the new
754- entries &mdash; you may need to wait for up to ten minutes before
755- you can download using the new password.
756- </p>
757 <form action="" method="post">
758 <button type="submit" name="regenerate" value="1">
759- Generate new password
760+ Reset password
761 </button>
762 </form>
763+ <p class="discreet">
764+ Note: after resetting the password for your personal
765+ access &mdash; and updating your sources.list with the new
766+ entries &mdash; you may need to wait for up to ten minutes before
767+ you can download using the new password.
768+ </p>
769 </div>
770 </tal:active>
771 </div>
772
773=== modified file 'lib/lp/soyuz/templates/person-archive-subscriptions.pt'
774--- lib/lp/soyuz/templates/person-archive-subscriptions.pt 2009-08-05 10:02:22 +0000
775+++ lib/lp/soyuz/templates/person-archive-subscriptions.pt 2009-09-30 14:18:17 +0000
776@@ -7,24 +7,20 @@
777 i18n:domain="launchpad"
778 >
779 <body>
780- <div metal:fill-slot="heading">
781- <h1 tal:content="CONTEXTS/fmt:pagetitle">
782- Private PPA access
783- </h1>
784- </div>
785
786 <div metal:fill-slot="main">
787 <div class="top-portlet">
788 <tal:has_subscriptions condition="view/subscriptions_with_tokens">
789 <div id="current_subscriptions">
790- <p>Here's a list of all the private archives to which you have
791- been granted access.
792+ <p>All the private archives to which you have
793+ been granted access are listed below.
794 </p>
795 <table summary="CONTEXTS/fmt:pagetitle" id="archive-subscriptions"
796- class="listing">
797+ class="listing" style="width:70%">
798 <thead>
799 <tr class="archive-subscription-row">
800 <th>Archive</th>
801+ <th></th>
802 </tr>
803 </thead>
804 <tbody>
805
806=== added directory 'lib/lp/soyuz/windmill'
807=== added file 'lib/lp/soyuz/windmill/__init__.py'
808=== added file 'lib/lp/soyuz/windmill/testing.py'
809--- lib/lp/soyuz/windmill/testing.py 1970-01-01 00:00:00 +0000
810+++ lib/lp/soyuz/windmill/testing.py 2009-09-30 14:18:17 +0000
811@@ -0,0 +1,18 @@
812+# Copyright 2009 Canonical Ltd. This software is licensed under the
813+# GNU Affero General Public License version 3 (see the file LICENSE).
814+
815+"""Soyuz-specific testing infrastructure for Windmill."""
816+
817+__metaclass__ = type
818+__all__ = [
819+ 'SoyuzWindmillLayer',
820+ ]
821+
822+
823+from canonical.testing.layers import BaseWindmillLayer
824+
825+
826+class SoyuzWindmillLayer(BaseWindmillLayer):
827+ """Layer for Soyuz Windmill tests."""
828+
829+ base_url = 'http://launchpad.dev:8085/'
830
831=== added directory 'lib/lp/soyuz/windmill/tests'
832=== added file 'lib/lp/soyuz/windmill/tests/__init__.py'
833=== added file 'lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py'
834--- lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py 1970-01-01 00:00:00 +0000
835+++ lib/lp/soyuz/windmill/tests/test_archivesubscribersindex.py 2009-09-30 14:18:17 +0000
836@@ -0,0 +1,96 @@
837+# Copyright 2009 Canonical Ltd. This software is licensed under the
838+# GNU Affero General Public License version 3 (see the file LICENSE).
839+
840+"""Test for the archive subscribers index page.."""
841+
842+__metaclass__ = type
843+__all__ = []
844+
845+import transaction
846+import unittest
847+
848+from windmill.authoring import WindmillTestClient
849+
850+from zope.component import getUtility
851+
852+from canonical.launchpad.ftests import login, logout
853+from canonical.launchpad.windmill.testing.lpuser import LaunchpadUser
854+from lp.registry.interfaces.distribution import IDistributionSet
855+from lp.soyuz.windmill.testing import SoyuzWindmillLayer
856+from lp.testing import TestCaseWithFactory
857+
858+WAIT_PAGELOAD = u'30000'
859+WAIT_ELEMENT_COMPLETE = u'30000'
860+WAIT_CHECK_CHANGE = u'1000'
861+ADD_ACCESS_LINK = u'//a[@class="js-action sprite add"]'
862+CHOOSE_SUBSCRIBER_LINK = u'//a[@id="show-widget-field-subscriber"]'
863+SUBSCRIBER_SEARCH_FIELD = (
864+ u'//div[@id="yui-pretty-overlay-modal"]//input[@name="search"]')
865+SUBSCRIBER_SEARCH_BUTTON = u'//div[@id="yui-pretty-overlay-modal"]//button'
866+FIRST_SUBSCRIBER_RESULT = (
867+ u'//div[@id="yui-pretty-overlay-modal"]'
868+ '//span[@class="yui-picker-result-title"]')
869+
870+
871+class TestArchiveSubscribersIndex(TestCaseWithFactory):
872+
873+ layer = SoyuzWindmillLayer
874+
875+ def setUp(self):
876+ """Create a private PPA."""
877+ super(TestArchiveSubscribersIndex, self).setUp()
878+
879+ user = self.factory.makePerson(
880+ name='joe-bloggs', email='joe@example.com', password='joe',
881+ displayname='Joe Bloggs')
882+ ubuntu = getUtility(IDistributionSet)['ubuntu']
883+ self.ppa = self.factory.makeArchive(
884+ owner=user, name='myppa', distribution=ubuntu)
885+
886+ login('foo.bar@canonical.com')
887+ self.ppa.private = True
888+ self.ppa.buildd_secret = 'secret'
889+ logout()
890+ transaction.commit()
891+
892+ self.lpuser = LaunchpadUser(
893+ 'Joe Bloggs', 'joe@example.com', 'joe')
894+
895+ def test_add_subscriber(self):
896+ """Test adding a private PPA subscriber.."""
897+ client = WindmillTestClient('Adding private PPA subscribers.')
898+
899+ self.lpuser.ensure_login(client)
900+
901+ client.open(url='http://launchpad.dev:8085/~joe-bloggs/'
902+ '+archive/myppa/+subscriptions')
903+ client.waits.forPageLoad(timeout=WAIT_PAGELOAD)
904+
905+ # Click on the JS add access action.
906+ client.waits.forElement(xpath=ADD_ACCESS_LINK)
907+ client.click(xpath=ADD_ACCESS_LINK)
908+
909+ # Open the picker, search for 'launchpad' and choose the first
910+ # result
911+ client.click(xpath=CHOOSE_SUBSCRIBER_LINK)
912+ client.type(xpath=SUBSCRIBER_SEARCH_FIELD, text='launchpad')
913+ client.click(xpath=SUBSCRIBER_SEARCH_BUTTON)
914+
915+ client.waits.forElement(xpath=FIRST_SUBSCRIBER_RESULT)
916+ client.click(xpath=FIRST_SUBSCRIBER_RESULT)
917+
918+ # Add the new subscriber.
919+ client.click(id='field.actions.add')
920+ client.waits.forPageLoad(timeout=WAIT_PAGELOAD)
921+
922+ # And verify that the correct informational message is displayed.
923+ # It would be nice if we could use ... here.
924+ client.asserts.assertText(
925+ xpath=u'//div[@class="informational message"]',
926+ validator='You have granted access for Launchpad Developers '
927+ 'to install software from PPA named myppa for Joe '
928+ 'Bloggs. Members of Launchpad Developers will be '
929+ 'notified of the access via email.')
930+
931+def test_suite():
932+ return unittest.TestLoader().loadTestsFromName(__name__)