Merge lp:~wallyworld/launchpad/admins-can-unsubscribe-bugs-2 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 13344
Proposed branch: lp:~wallyworld/launchpad/admins-can-unsubscribe-bugs-2
Merge into: lp:launchpad
Diff against target: 1175 lines (+107/-914)
9 files modified
lib/canonical/launchpad/windmill/jstests/initialize.js (+0/-164)
lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js (+0/-652)
lib/canonical/launchpad/windmill/jstests/login.js (+0/-32)
lib/lp/bugs/browser/bugsubscription.py (+1/-1)
lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+64/-0)
lib/lp/bugs/javascript/subscribers_list.js (+2/-39)
lib/lp/bugs/javascript/tests/test_subscribers_list.js (+28/-14)
lib/lp/bugs/stories/bug-privacy/05-set-bug-private-as-admin.txt (+3/-3)
lib/lp/bugs/stories/bugs/bug-add-subscriber.txt (+9/-9)
To merge this branch: bzr merge lp:~wallyworld/launchpad/admins-can-unsubscribe-bugs-2
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+66309@code.launchpad.net

Commit message

[r=danilo][bug=633,134577] Allow a person to unsubscribe from a bug those people/teams who they have subscribed.

Description of the change

This branch provides the client side code to allow someone to unsubscribe from a bug a person/team they have subscribed. A previous branch implemented the functionality on the model. This branch uses that to provide the unsubscribe link when appropriate.

== Implementation ==

The BugPortletSubscribersWithDetails view was changed to call BugSubscription.canBeUnsubscribedByUser() to see if the logged in user can unsubscribe someone.
The subscribers_list javascript was changed to always add the unsubscribe link when a subscription request is completed successfully.

== Demo and QA ==

Subscribe someone to a bug - the unsubscribe icon should be rendered in the subscription list to allow you to unsubscribe them.

== Tests ==

Added tests to BugPortletSubscribersWithDetailsTests:
test_data_subscription_lp_admin
test_data_person_subscription_subscriber

Modify the test_subscribeSomeoneElse_success test in test_subscribers_list.js test

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bugsubscription.py
  lib/lp/bugs/browser/tests/test_bugsubscription_views.py
  lib/lp/bugs/javascript/subscribers_list.js
  lib/lp/bugs/javascript/tests/test_subscribers_list.js

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :

Looks great, Ian, thanks for fixing this!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed directory 'lib/canonical/launchpad/windmill'
2=== removed directory 'lib/canonical/launchpad/windmill/jstests'
3=== removed file 'lib/canonical/launchpad/windmill/jstests/initialize.js'
4--- lib/canonical/launchpad/windmill/jstests/initialize.js 2010-11-16 18:54:59 +0000
5+++ lib/canonical/launchpad/windmill/jstests/initialize.js 1970-01-01 00:00:00 +0000
6@@ -1,164 +0,0 @@
7-/*
8-Copyright 2009 Canonical Ltd. This software is licensed under the
9-GNU Affero General Public License version 3 (see the file LICENSE).
10-
11-Contains the JS fixtures we use to test AJAX call in windmill.
12-
13-Since AJAX calls will complete asynchronously, we need to use a
14-synchronisation mechanism.
15-
16- */
17-
18-
19-/* A synchronized test.
20- * @param name The name of the test, the DOM node used for synchronization
21- * will use that name.
22- *
23- * @param body This is a list of functions or strings or windmill action
24- * object. That makes this test body.
25- *
26- * @param debug (Optional) If this is set to true, tracing information
27- * will be logged on the Firebug console.
28- */
29-function SynchronizedTest(name, body, debug) {
30- this.name = name;
31- this.debug = !!debug;
32- this.synced = false;
33-
34- /* Create the test body. */
35- this.test_body = [];
36- this.add_windmill_actions(this.test_body, body);
37-
38- /*
39- * The tear down is also as an array, so that many cleanup
40- * actions can be tacked to it using add_cleanups()
41- */
42- // Windmill doesn't like empty array teardown. So we define it as an
43- // empty object, which gets replaced by an array when add_cleanups is
44- // called.
45- //this.teardown = [];
46- this.teardown = function () {};
47-}
48-
49-
50-/* The YUI instance used by SynchronizedTest. */
51-SynchronizedTest.prototype.Y = YUI({
52- bootstrap: false,
53- fetchCSS: false,
54- combine: false,
55- timeout: 50
56- }).use('dump');
57-
58-/* Create the synchronization node, that should make the wait() caller
59- * return.
60- *
61- * @param result This object will be available from the result attribute.
62- * This can be used to provide information passed to the callback
63- * and that we want to make assertion on.
64- */
65-SynchronizedTest.prototype.sync = function (result) {
66- this.log('sync() called with ' + this.Y.dump(result));
67- this.result = result;
68- this.synced = true;
69-};
70-
71-
72-/* Convert a sequence of test items and adds them to a windmill array
73- * test specification.
74- *
75- * That method is used by the constructor and add_cleanups method.
76- */
77-SynchronizedTest.prototype.add_windmill_actions = function (list, items) {
78- var test = this;
79-
80- /* Windmill invokes all test functions without setting the this parameter.
81- * So we create wrapper that will pass it to our functions.
82- */
83- function create_test_wrapper (func) {
84- return function () { func(test); };
85- }
86-
87- for (var i=0; i< items.length; i++) {
88- var test_item = items[i];
89- var yui_lang = this.Y.Lang;
90- if (yui_lang.isFunction(test_item)) {
91- //Create a wrapper that passes the test as first parameter.
92- list.push(create_test_wrapper(test_item));
93- } else if (yui_lang.isString(test_item)) {
94- //This calls a method on the test. And sticks the result
95- //in the test body. Common use case is to use 'wait_action' to
96- //add a windmill wait action for the synchronizing condition.
97- var action = test[test_item].call(test);
98- list.push(action);
99- } else if (yui_lang.isObject(test_item)) {
100- //We expect this to be a Windmill action.
101- list.push(test_item);
102- } else {
103- throw new Error(
104- 'Unknown test predicate: ' + this.Y.dump(test_item));
105- }
106- }
107-};
108-
109-/* Add functions/actions to the "teardown" test.
110- *
111- * @param cleanups An array of functions, or strings or objects representing
112- * windmill actions. (Like in the test body parameter.)
113- */
114-SynchronizedTest.prototype.add_cleanups = function (cleanups) {
115- if (this.Y.Lang.isFunction(this.teardown)) {
116- this.teardown = [];
117- }
118- this.add_windmill_actions(this.teardown, cleanups);
119-};
120-
121-/* Return a windmill action that can be used to wait for
122- * the synchronization to happen.
123- */
124-SynchronizedTest.prototype.wait_action = function () {
125- var test = this;
126- return {
127- method: "waits.forJS",
128- params: {
129- //The function waits for the synced attribute to be set to true;
130- //and then resets it so that next synchronization action work.
131- js: function () {
132- if (test.synced) {
133- test.synced = false;
134- return true;
135- } else {
136- return false;
137- }
138- },
139- timeout: 8000
140- }
141- };
142-};
143-
144-
145-/* Output a log message when debug is turned on.
146- */
147-SynchronizedTest.prototype.log = function (message) {
148- if (this.debug && console) {
149- console.log(this.name + ': ' + message);
150- }
151-};
152-
153-
154-/* Return a configuration object that can be used as a on
155- * specification to YUI.io.
156- *
157- * It basically will call the test sync method, and save the
158- * name of the handler called, and the arguments list.
159- */
160-SynchronizedTest.prototype.create_yui_sync_on = function () {
161- var test = this;
162- return {
163- success: function () {
164- test.sync({callback: 'success', args: test.Y.Array(arguments)});
165- },
166- failure: function () {
167- test.sync({callback: 'failure', args: test.Y.Array(arguments)});
168- }
169- };
170-};
171
172=== removed file 'lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js'
173--- lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js 2011-02-24 22:43:20 +0000
174+++ lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js 1970-01-01 00:00:00 +0000
175@@ -1,652 +0,0 @@
176-// Unit testing framework for the Launchpad AJAX test suite.
177-var client = new LP.client.Launchpad();
178-
179-windmill.jsTest.require('login.js');
180-
181-
182-// Test access to prepopulated data.
183-test_prepopulated_data_anonymous = new SynchronizedTest(
184- 'test_prepopulated_data_anonymous', [
185- {"params": {"url": "/firefox/+bug/1"}, "method": "open"},
186- {"params": {}, "method": "waits.forPageLoad"},
187- // Wait until one of the last element on the page is available.
188- {"params": {"xpath": "//div[@id='globalfooter']"},
189- "method": "waits.forElement"},
190- function (test) {
191- jum.assertUndefined(LP.links.me);
192- jum.assertUndefined(LP.cache.context);
193- }
194- ]);
195-
196-test_logged_in_as_foo_bar.test_prepopulated_data = new SynchronizedTest(
197- 'test_prepopulated_data', [
198- {"params": {"url": "/firefox/+bug/1"}, "method": "open"},
199- {"params": {}, "method": "waits.forPageLoad"},
200- // Wait until one of the last element on the page is available.
201- {"params": {"xpath": "//div[@id='globalfooter']"},
202- "method": "waits.forElement"},
203- function (test) {
204- // If the user is logged in, the link to their user account
205- // will be in the link cache.
206- jum.assertEquals(LP.links.me, '/~name16');
207-
208- // If the view has a context, the context will be in the object
209- // cache.
210- jum.assertNotUndefined(LP.cache.context);
211- var context = LP.cache.context;
212- jum.assertNotEquals(context.self_link.indexOf(
213- "/api/devel/firefox/+bug/1"), -1);
214- jum.assertNotEquals(context.resource_type_link.indexOf(
215- "/api/devel/#bug_task"), -1);
216- jum.assertNotEquals(context.owner_link.indexOf(
217- "/api/devel/~name12"), -1);
218- jum.assertNotEquals(context.related_tasks_collection_link.indexOf(
219- "/api/devel/firefox/+bug/1/related_tasks"), -1);
220-
221- // Specific views may add additional objects to the object cache
222- // or links to the link cache.
223- var bug = LP.cache.bug;
224- jum.assertNotUndefined(LP.cache.bug);
225- jum.assertNotEquals(bug.self_link.indexOf("/api/devel/bugs/1"), -1);
226- }
227- ]);
228-
229-// Test that making a web service call doesn't modify the callback
230-// methods you pass in.
231-var test_callback_safety = function() {
232- var success_callback = function() {};
233- var config = {on: {success: success_callback}};
234-
235- // Test a GET.
236- client.get("/people", config);
237- jum.assertTrue(success_callback === config.on.success);
238-
239- // Test a named POST.
240- name = "callback-safety-test" + new Date().getTime();
241- config.parameters = {display_name: 'My new team', name: name};
242- client.named_post("/people", "newTeam", config);
243- jum.assertTrue(success_callback === config.on.success);
244-};
245-
246-
247-var test_normalize_uri = function() {
248- var normalize = LP.client.normalize_uri;
249- jum.assertEquals(normalize("http://www.example.com/api/devel/foo"),
250- "/api/devel/foo");
251- jum.assertEquals(normalize("http://www.example.com/foo/bar"), "/foo/bar");
252- jum.assertEquals(normalize("/foo/bar"), "/api/devel/foo/bar");
253- jum.assertEquals(normalize("/api/devel/foo/bar"), "/api/devel/foo/bar");
254- jum.assertEquals(normalize("foo/bar"), "/api/devel/foo/bar");
255- jum.assertEquals(normalize("api/devel/foo/bar"), "/api/devel/foo/bar");
256-};
257-
258-var test_append_qs = function() {
259- var qs = "";
260- qs = LP.client.append_qs(qs, "Pöllä", "Perelló");
261- jum.assertEquals("P%C3%B6ll%C3%A4=Perell%C3%B3", qs);
262-};
263-
264-var test_field_uri = function() {
265- jum.assertEquals(LP.client.get_field_uri("http://www.example.com/api/devel/foo", "field"),
266- "/api/devel/foo/field");
267- jum.assertEquals(LP.client.get_field_uri("/no/slash", "field"),
268- "/api/devel/no/slash/field");
269- jum.assertEquals(LP.client.get_field_uri("/has/slash/", "field"),
270- "/api/devel/has/slash/field");
271-};
272-
273-// Test that retrieving a non-existent resource uses the failure handler.
274-var test_no_such_url = new SynchronizedTest('test_no_such_url', [
275- function (test) {
276- client.get("no-such-url", {on: test.create_yui_sync_on()});
277- },
278- 'wait_action',
279- function (test) {
280- jum.assertEquals('failure', test.result.callback);
281- }
282- ]);
283-
284-
285-//Check that retrieving the root by relative URL returns the root entry.
286-var test_relative_url = new SynchronizedTest('test_relative_url', [
287- function (test) {
288- client.get("", {on: test.create_yui_sync_on()});
289- },
290- 'wait_action',
291- function (test) {
292- jum.assertEquals('success', test.result.callback);
293- var launchpad = test.result.args[0];
294- jum.assertTrue(launchpad instanceof LP.client.Root);
295- jum.assertNotNull(
296- launchpad.people_collection_link.match(/people$/));
297- }
298- ]);
299-
300-
301-//Check that retrieving the root by absolute URL returns the root entry.
302-var test_absolute_url = new SynchronizedTest('test_absolute_url', [
303- function (test) {
304- client.get("", {on: test.create_yui_sync_on()});
305- },
306- 'wait_action',
307- function (test) {
308- jum.assertEquals('success', test.result.callback);
309- var launchpad = test.result.args[0];
310- jum.assertTrue(launchpad instanceof LP.client.Root);
311- jum.assertNotNull(
312- launchpad.people_collection_link.match(/people$/));
313- }
314- ]);
315-
316-
317-// Check that collection resources are paginated.
318-var test_pagination_collection = new SynchronizedTest(
319- 'test_pagination_collection', [
320- function (test) {
321- client.get("/people", {on: test.create_yui_sync_on(),
322- start: 2, size: 1});
323- },
324- 'wait_action',
325- function (test) {
326- jum.assertEquals('success', test.result.callback);
327- var people = test.result.args[0];
328- jum.assertEquals(2, people.start);
329- jum.assertEquals(1, people.entries.length);
330- }
331- ]);
332-
333-
334-// Check invoking a read-only name operation on a resource URI.
335-// Make sure it's possible to invoke named operations on URIs.
336-var test_named_get = new SynchronizedTest('test_named_get', [
337- function (test) {
338- client.named_get(
339- "people/", "find", {on: test.create_yui_sync_on(),
340- parameters: {text:"salgado"}});
341- },
342- 'wait_action',
343- function (test) {
344- jum.assertEquals('success', test.result.callback);
345- var collection = test.result.args[0];
346- jum.assertTrue(collection instanceof LP.client.Collection);
347- jum.assertEquals(1, collection.total_size);
348- }
349- ]);
350-
351-
352-
353-var test_named_get_returning_json = new SynchronizedTest(
354- 'test_named_get_returning_json', [
355- // Make sure a named GET that returns a JSON data structure works.
356- function (test) {
357- client.named_get("ubuntu/+archive/primary", "getBuildCounters",
358- {on: test.create_yui_sync_on()});
359- },
360- 'wait_action',
361- function (test) {
362- jum.assertEquals('success', test.result.callback);
363- var structure = test.result.args[0];
364- jum.assertEquals(structure.failed, 5);
365- }
366- ]);
367-
368-
369-// Check the invocation of a named POST operation.
370-test_logged_in_as_foo_bar.test_named_post = new SynchronizedTest(
371- 'test_named_post', [
372- function (test) {
373- test.bugtask_uri = '/redfish/+bug/15';
374- client.named_post(
375- test.bugtask_uri, 'transitionToStatus',
376- {on: test.create_yui_sync_on(),
377- parameters: {status: 'Confirmed'}});
378- },
379- 'wait_action',
380- function (test) {
381- jum.assertEquals('success', test.result.callback);
382- test.add_cleanups([
383- function (test) {
384- client.named_post(
385- test.bugtask_uri, 'transitionToStatus',
386- {on: test.create_yui_sync_on(),
387- parameters: {'status': 'New'}});
388- },
389- 'wait_action']);
390- // Get the bugtask and make sure its status has changed.
391- client.get(test.bugtask_uri, {on: test.create_yui_sync_on()});
392- },
393- 'wait_action',
394- function(test) {
395- jum.assertEquals('success', test.result.callback);
396- var bugtask = test.result.args[0];
397- jum.assertEquals(bugtask.get('status'), 'Confirmed');
398- }
399- ]);
400-
401-//Test that follow_link return the resource at the end of the link.
402-// Retrieve the launchpad root and check the people link is a collection.
403-var test_follow_link = new SynchronizedTest('test_follow_link', [
404- function (test) {
405- client.get("", {on: test.create_yui_sync_on()});
406- },
407- 'wait_action',
408- function (test) {
409- jum.assertEquals('success', test.result.callback);
410- var root_object = test.result.args[0];
411- root_object.follow_link('people', {on: test.create_yui_sync_on()});
412- },
413- 'wait_action',
414- function (test) {
415- jum.assertEquals('success', test.result.callback);
416- var people = test.result.args[0];
417- jum.assertTrue(people instanceof LP.client.Collection);
418- jum.assertEquals(4, people.total_size);
419- }
420- ]);
421-
422-//Test that follow_link follows through redirect.
423-// Retrieve the launchpad root and check the people
424-test_logged_in_as_foo_bar.test_follow_link_through_redirect =
425- new SynchronizedTest(
426- 'test_follow_link_through_redirect', [
427- function (test) {
428- client.get("", {on: test.create_yui_sync_on()});
429- },
430- 'wait_action',
431- function (test) {
432- jum.assertEquals('success', test.result.callback);
433- var root_object = test.result.args[0];
434- root_object.follow_link('me', {on: test.create_yui_sync_on()});
435- },
436- 'wait_action',
437- function (test) {
438- jum.assertEquals('success', test.result.callback);
439- var me = test.result.args[0];
440- jum.assertTrue(me instanceof LP.client.Entry);
441- jum.assertEquals('name16', me.get('name'));
442- }
443- ]);
444-
445-
446-//Test that retrieving an entry resource yield an Entry object.
447-var test_entry_get = new SynchronizedTest('test_entry_get', [
448- function (test) {
449- client.get('~salgado', {on: test.create_yui_sync_on()});
450- },
451- 'wait_action',
452- function (test) {
453- jum.assertEquals('success', test.result.callback);
454- var salgado = test.result.args[0];
455- jum.assertTrue(salgado instanceof LP.client.Entry);
456- jum.assertEquals("salgado", salgado.get('name'));
457- }
458- ]);
459-
460-// Test that retrieving an HTML representation of an entry yields an
461-// HTML snippet.
462-var test_entry_html_get = new SynchronizedTest('test_entry_html_get', [
463- function (test) {
464- client.get('~salgado', {on: test.create_yui_sync_on(),
465- accept: LP.client.XHTML});
466- },
467- 'wait_action',
468- function (test) {
469- jum.assertEquals('success', test.result.callback);
470- var salgado_html = test.result.args[0];
471- jum.assertNotEquals(salgado_html.indexOf("<dl"), -1);
472- }
473- ]);
474-
475-
476-// Test that it's possible to request an HTML representation of
477-// an object when updating it.
478-test_logged_in_as_foo_bar.test_html_entry_lp_save = new SynchronizedTest(
479- 'test_entry_lp_save', [
480- function (test) {
481- client.get('~salgado', {on: test.create_yui_sync_on()});
482- },
483- 'wait_action',
484- function (test) {
485- jum.assertEquals('success', test.result.callback);
486- var salgado = test.result.args[0];
487- salgado.lp_save({on: test.create_yui_sync_on(),
488- accept: LP.client.XHTML});
489- },
490- 'wait_action',
491- function (test) {
492- jum.assertEquals('success', test.result.callback);
493- var salgado_html = test.result.args[0];
494- jum.assertNotEquals(salgado_html.indexOf("<dl"), -1);
495-
496- // Now test the patch() method directly.
497- client.patch('~salgado', {},
498- {on: test.create_yui_sync_on(),
499- accept: LP.client.XHTML});
500- },
501- 'wait_action',
502- function (test) {
503- jum.assertEquals('success', test.result.callback);
504- var salgado_html = test.result.args[0];
505- jum.assertNotEquals(salgado_html.indexOf("<dl"), -1);
506-
507- // Now test the patch() method on a field resource.
508- var field_uri = LP.client.get_field_uri('~salgado', 'display_name');
509- client.patch(field_uri, 'Guilherme Salgado 2',
510- {on: test.create_yui_sync_on(),
511- accept: LP.client.XHTML});
512- },
513- 'wait_action',
514- function (test) {
515- var field_uri = LP.client.get_field_uri('~salgado', 'display_name');
516- jum.assertEquals('success', test.result.callback);
517- var salgado_name_html = test.result.args[0];
518- jum.assertEquals(salgado_name_html, "Guilherme Salgado 2");
519-
520- // Now make sure patch() on a field resource works when we
521- // request a JSON representation in return.
522- field_uri = LP.client.get_field_uri('~salgado', 'display_name');
523- client.patch(field_uri, 'Guilherme Salgado',
524- {on: test.create_yui_sync_on()});
525- },
526- 'wait_action',
527- function (test) {
528- var field_uri = LP.client.get_field_uri('~salgado', 'display_name');
529- jum.assertEquals('success', test.result.callback);
530- var salgado_name_html = test.result.args[0];
531- jum.assertEquals(salgado_name_html, "Guilherme Salgado");
532- }
533-]);
534-
535-//Test that modifying an entry and then calling lp_save() saves the
536-//entry on the server.
537-test_logged_in_as_foo_bar.test_entry_lp_save = new SynchronizedTest(
538- 'test_entry_lp_save', [
539- function (test) {
540- client.get('~salgado', {on: test.create_yui_sync_on()});
541- },
542- 'wait_action',
543- function (test) {
544- jum.assertEquals('success', test.result.callback);
545- var salgado = test.result.args[0];
546- test.original_display_name = salgado.get('display_name');
547- salgado.set('display_name', '<b>A new display name</b>');
548- salgado.lp_save({on: test.create_yui_sync_on()});
549- },
550- 'wait_action',
551- function (test) {
552- jum.assertEquals('success', test.result.callback);
553- // Make sure that the save operation returned a new version of
554- // the object.
555- var new_salgado = test.result.args[0];
556- jum.assertEquals(new_salgado.get('display_name'),
557- '<b>A new display name</b>');
558-
559- test.add_cleanups([
560- function (test) {
561- client.get('~salgado', {on: test.create_yui_sync_on()});
562- },
563- 'wait_action',
564- function (test) {
565- jum.assertEquals('success', test.result.callback);
566- var salgado = test.result.args[0];
567- salgado.set('display_name', test.original_display_name);
568- salgado.lp_save({on: test.create_yui_sync_on()});
569- jum.assertEquals(salgado.dirty_attributes.length, 0);
570- },
571- 'wait_action']);
572- client.get('~salgado', {on: test.create_yui_sync_on()});
573- },
574- 'wait_action',
575- function (test) {
576- jum.assertEquals('success', test.result.callback);
577- var salgado = test.result.args[0];
578- jum.assertEquals('<b>A new display name</b>',
579- salgado.get('display_name'));
580-
581- // As long as we've got bad HTML in the display name, let's
582- // get an HTML representation and see whether the bad HTML was
583- // escaped.
584- client.get('~salgado', {on: test.create_yui_sync_on(),
585- accept: LP.client.XHTML});
586- },
587- 'wait_action',
588- function (test) {
589- jum.assertEquals('success', test.result.callback);
590-
591- var salgado_html = test.result.args[0];
592- jum.assertNotEquals(salgado_html.indexOf("<dl"), -1);
593- jum.assertNotEquals(salgado_html.indexOf("&lt;b&gt;A new"), -1);
594-
595- // Now test the patch() method directly.
596- client.patch('~salgado', {'display_name': 'A patched display name'},
597- {on: test.create_yui_sync_on()});
598- },
599- 'wait_action',
600- function (test) {
601- jum.assertEquals('success', test.result.callback);
602- client.get('~salgado', {on: test.create_yui_sync_on()});
603- },
604- 'wait_action',
605- function (test) {
606- jum.assertEquals('success', test.result.callback);
607- var salgado = test.result.args[0];
608- jum.assertEquals('A patched display name', salgado.get('display_name'));
609-
610- // Test that a mismatched ETag results in a failed save.
611- salgado.set('http_etag', "Non-matching ETag.");
612- salgado.set('display_name', "This display name will not be set.");
613- salgado.lp_save({on: test.create_yui_sync_on()});
614- },
615- 'wait_action',
616- function (test) {
617- jum.assertEquals('failure', test.result.callback);
618- var xhr = test.result.args[1];
619- jum.assertEquals(xhr.status, 412);
620- }
621- ]);
622-
623-
624-//Test retrieving a collection object.
625-var test_collection = new SynchronizedTest('test_collection', [
626- function (test) {
627- client.get("people", {on: test.create_yui_sync_on()});
628- },
629- 'wait_action',
630- function (test) {
631- jum.assertEquals('success', test.result.callback);
632- var collection = test.result.args[0];
633- jum.assertTrue(collection instanceof LP.client.Collection);
634- jum.assertEquals(4, collection.total_size);
635- },
636- 'wait_action',
637- function (test) {
638- jum.assertEquals('success', test.result.callback);
639- var entries = test.result.args[0].entries,
640- length = test.result.args[0].total_size,
641- index;
642- for (index = 0 ; index < length ; index++) {
643- jum.assertTrue(entries[index] instanceof LP.client.Entry);
644- }
645- },
646- 'wait_action',
647- function (test) {
648- jum.assertEquals('success', test.result.callback);
649- var entry = test.result.args[0].entries[0];
650- jum.assertEquals('test', entry.display_name);
651- entry.set('display_name', "Set Display Name");
652- entry.lp_save({on: test.create_yui_sync_on()});
653- },
654- 'wait_action',
655- function (test) {
656- jum.assertEquals('success', test.result.callback);
657- client.get('people', { on: test.create_yui_sync_on()});
658- },
659- 'wait_action',
660- function (test) {
661- jum.assertEquals('success', test.result.callback);
662- var entry = test.result.args[0].entries[0];
663- jum.assertEquals('Set Display Name', entry.display_name);
664- }
665- ]);
666-
667-
668-//Test the lp_slice() method on a collection.
669-var test_collection_lp_slice = new SynchronizedTest(
670- 'test_collection_lp_slice', [
671- function (test) {
672- client.get("people", {on: test.create_yui_sync_on()});
673- },
674- 'wait_action',
675- function (test) {
676- jum.assertEquals('success', test.result.callback);
677- var collection = test.result.args[0];
678- collection.lp_slice(test.create_yui_sync_on(), 2, 1);
679- },
680- 'wait_action',
681- function (test) {
682- jum.assertEquals('success', test.result.callback);
683- var slice = test.result.args[0];
684- jum.assertEquals(2, slice.start);
685- jum.assertEquals(1, slice.entries.length);
686- }
687- ]);
688-
689-
690-//Test invoking a named GET on a collection.
691-var test_collection_named_get = new SynchronizedTest(
692- 'test_collection_named_get', [
693- function (test) {
694- client.get("people", {on: test.create_yui_sync_on() });
695- },
696- 'wait_action',
697- function (test) {
698- jum.assertEquals('success', test.result.callback);
699- var collection = test.result.args[0];
700- collection.named_get(
701- 'find', {on: test.create_yui_sync_on(),
702- parameters: {text: 'salgado'}});
703- },
704- 'wait_action',
705- function (test) {
706- jum.assertEquals('success', test.result.callback);
707- var collection = test.result.args[0];
708- jum.assertTrue(collection instanceof LP.client.Collection);
709- jum.assertEquals(1, collection.total_size);
710- }
711- ]);
712-
713-
714-//Test named POST on a collection, and object creation.
715-test_logged_in_as_foo_bar.test_collection_named_post = new SynchronizedTest(
716- 'test_collection_named_post', [
717- function(test) {
718- client.get("/people/", {on: test.create_yui_sync_on()});
719- },
720- 'wait_action',
721- function(test) {
722- // Generate a unique team name so that the team-creation test
723- // can be run multiple times without resetting the dataset.
724- name = "newteam" + new Date().getTime();
725- var collection = test.result.args[0];
726- collection.named_post('newTeam',
727- {on: test.create_yui_sync_on(),
728- parameters: {display_name: 'My new team',
729- name: name}});
730- },
731- 'wait_action',
732- function(test) {
733- var new_entry = test.result.args[0];
734- jum.assertEquals("success", test.result.callback);
735- jum.assertTrue(new_entry instanceof LP.client.Entry);
736- jum.assertEquals(new_entry.get("display_name"), "My new team");
737- jum.assertNotEquals(new_entry.lp_original_uri.indexOf("/~newteam"),
738- -1);
739- }
740- ]);
741-
742-//Test paging on a named collection.
743-var test_collection_paged_named_get = new SynchronizedTest(
744- 'test_collection_paged_named_get', [
745- function (test) {
746- client.get("people", {on: test.create_yui_sync_on() });
747- },
748- 'wait_action',
749- function (test) {
750- jum.assertEquals('success', test.result.callback);
751- var collection = test.result.args[0];
752- collection.named_get(
753- 'find', {on: test.create_yui_sync_on(),
754- parameters: {text: 'salgado'},
755- start: 10});
756- },
757- 'wait_action',
758- function (test) {
759- jum.assertEquals('success', test.result.callback);
760- var collection = test.result.args[0];
761- jum.assertTrue(collection instanceof LP.client.Collection);
762- jum.assertEquals(1, collection.total_size);
763- }
764- ]);
765-
766-// Test hosted file objects.
767-
768-// To test PUT to a hosted file we need to create a brand new
769-// file. Several problems combine to make this necessary. The
770-// first is that you can't send a binary file through XHR: it gets
771-// truncated at the first null character. So we can't just PUT to
772-// a mugshot or icon. There are some product release files in the
773-// preexisting dataset, but they don't have anything backing them
774-// in the librarian, so we can't get a proper handle on them. So
775-// we need to create a brand new file and then test PUT on it.
776-
777-// Unfortunately, currently there are no files you can create with
778-// PUT, so we can't test this.
779-
780-test_logged_in_as_foo_bar.test_hosted_files = new SynchronizedTest(
781-
782- 'test_hosted_files', [
783- function(test) {
784- var bug_uri = '/bugs/15';
785- client.named_post(bug_uri, 'addAttachment',
786- {on: test.create_yui_sync_on(),
787- parameters: {comment: 'A new attachment',
788- content_type: 'text/plain',
789- data: 'Some data.',
790- filename: 'foo.txt'}});
791- },
792- 'wait_action',
793- function(test) {
794- jum.assertEquals('success', test.result.callback);
795- var attachment = test.result.args[0];
796- attachment.follow_link('data',
797- {on: test.create_yui_sync_on()});
798- },
799- 'wait_action',
800- function(test) {
801- jum.assertEquals('success', test.result.callback);
802- var hosted_file = test.result.args[0];
803-
804-// Unfortunately, there's no hosted file that can be edited through
805-// the web service, so we can't test PUT.
806-// hosted_file.contents = "['Unit tester was here.']";
807-// hosted_file.filename = "unittest.json";
808-// hosted_file.content_type = "application/json";
809-// hosted_file.lp_save({on: test.create_yui_sync_on()});
810-// },
811-// 'wait_action',
812-// function(test) {
813-// jum.assertEquals('success', test.result.callback);
814-// var hosted_file = test.result.args[2];
815- hosted_file.lp_delete({on: test.create_yui_sync_on()});
816- },
817- 'wait_action',
818- function(test) {
819- jum.assertEquals('failure', test.result.callback);
820- // XXX flacoste 2008/12/12 bug=307539
821- // This code works right now, but when testing a hosted file
822- // that can be edited through the web service, it will fail.
823- var request = test.result.args[1];
824- jum.assertEquals(405, request.status);
825- }
826- ]
827- );
828
829=== removed file 'lib/canonical/launchpad/windmill/jstests/login.js'
830--- lib/canonical/launchpad/windmill/jstests/login.js 2009-06-30 21:06:27 +0000
831+++ lib/canonical/launchpad/windmill/jstests/login.js 1970-01-01 00:00:00 +0000
832@@ -1,32 +0,0 @@
833-/*
834-Copyright 2009 Canonical Ltd. This software is licensed under the
835-GNU Affero General Public License version 3 (see the file LICENSE).
836-
837-Namespaces for tests requiring to be logged in.
838-*/
839-
840-
841-/* Logged in as Foo Bar. */
842-var test_logged_in_as_foo_bar = {};
843-test_logged_in_as_foo_bar.setup = [
844- {"params": {"link": "Log in \/ Register"},
845- "method": "asserts.assertNode"},
846- {"params": {"link": "Log in \/ Register"}, "method": "click"},
847- {"params": {}, "method": "waits.forPageLoad"},
848- {"params": {"id": "email"}, "method": "waits.forElement"},
849- {"params": {"text": "foo.bar@canonical.com", "id": "email"},
850- "method": "type"},
851- {"params": {"text": "test", "id": "password"}, "method": "type"},
852- {"params": {"name": "loginpage_submit_login"}, "method": "click"},
853- {"params": {}, "method": "waits.forPageLoad"},
854- {"params": {"link": "Foo Bar"}, "method": "waits.forElement"}
855- ];
856-
857-test_logged_in_as_foo_bar.teardown = [
858- {"params": {"name": "logout"}, "method": "click"},
859- // We need the waits.forPageLoad here because it's likely that the
860- // xpath expression might match on the previous page.
861- {"params": {}, "method": "waits.forPageLoad"},
862- {"params": {"xpath": "\/html\/body[@id='document']\/div[@id='mainarea']\/div[@id='container']\/div"}, "method": "waits.forElement"},
863- {"params": {"xpath": "\/html\/body[@id='document']\/div[@id='mainarea']\/div[@id='container']\/div", "validator": "You have been logged out"}, "method": "asserts.assertText"}
864- ];
865
866=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
867--- lib/lp/bugs/browser/bugsubscription.py 2011-06-17 10:44:18 +0000
868+++ lib/lp/bugs/browser/bugsubscription.py 2011-06-30 22:44:42 +0000
869@@ -540,7 +540,7 @@
870 details = list(bug.getDirectSubscribersWithDetails())
871 api_request = IWebServiceClientRequest(self.request)
872 for person, subscription in details:
873- can_edit = self.user is not None and self.user.inTeam(person)
874+ can_edit = subscription.canBeUnsubscribedByUser(self.user)
875 if person == self.user or (person.private and not can_edit):
876 # Skip the current user viewing the page,
877 # and private teams user is not a member of.
878
879=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
880--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-17 10:31:55 +0000
881+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-30 22:44:42 +0000
882@@ -7,6 +7,7 @@
883
884 from simplejson import dumps
885
886+from zope.component import getUtility
887 from zope.traversing.browser import absoluteURL
888
889 from canonical.launchpad.ftests import LaunchpadFormHarness
890@@ -19,10 +20,12 @@
891 BugSubscriptionSubscribeSelfView,
892 )
893 from lp.bugs.enum import BugNotificationLevel
894+from lp.registry.interfaces.person import IPersonSet
895 from lp.testing import (
896 person_logged_in,
897 TestCaseWithFactory,
898 )
899+from lp.testing.sampledata import ADMIN_EMAIL
900 from lp.testing.views import create_initialized_view
901
902
903@@ -573,6 +576,67 @@
904 self.assertEqual(
905 dumps([expected_result]), harness.view.subscriber_data_js)
906
907+ def test_data_subscription_lp_admin(self):
908+ # For a subscription, subscriber_data_js has can_edit
909+ # set to true for a Launchpad admin.
910+ bug = self._makeBugWithNoSubscribers()
911+ member = self.factory.makePerson()
912+ subscriber = self.factory.makePerson(
913+ name='user', displayname='Subscriber Name')
914+ with person_logged_in(member):
915+ bug.subscribe(subscriber, subscriber,
916+ level=BugNotificationLevel.LIFECYCLE)
917+ harness = LaunchpadFormHarness(
918+ bug, BugPortletSubscribersWithDetails)
919+ api_request = IWebServiceClientRequest(harness.request)
920+
921+ expected_result = {
922+ 'subscriber': {
923+ 'name': 'user',
924+ 'display_name': 'Subscriber Name',
925+ 'is_team': False,
926+ 'can_edit': True,
927+ 'web_link': canonical_url(subscriber),
928+ 'self_link': absoluteURL(subscriber, api_request),
929+ },
930+ 'subscription_level': "Lifecycle",
931+ }
932+
933+ # Login as admin
934+ admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
935+ with person_logged_in(admin):
936+ self.assertEqual(
937+ dumps([expected_result]), harness.view.subscriber_data_js)
938+
939+ def test_data_person_subscription_subscriber(self):
940+ # For a subscription, subscriber_data_js has can_edit
941+ # set to true for the subscriber.
942+ bug = self._makeBugWithNoSubscribers()
943+ subscriber = self.factory.makePerson(
944+ name='user', displayname='Subscriber Name')
945+ subscribed_by = self.factory.makePerson(
946+ name='someone', displayname='Subscribed By Name')
947+ with person_logged_in(subscriber):
948+ bug.subscribe(subscriber, subscribed_by,
949+ level=BugNotificationLevel.LIFECYCLE)
950+ harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
951+ api_request = IWebServiceClientRequest(harness.request)
952+
953+ expected_result = {
954+ 'subscriber': {
955+ 'name': 'user',
956+ 'display_name': 'Subscriber Name',
957+ 'is_team': False,
958+ 'can_edit': True,
959+ 'web_link': canonical_url(subscriber),
960+ 'self_link': absoluteURL(subscriber, api_request),
961+ },
962+ 'subscription_level': "Lifecycle",
963+ }
964+ with person_logged_in(subscribed_by):
965+ self.assertEqual(
966+ dumps([expected_result]), harness.view.subscriber_data_js)
967+
968 def test_data_person_subscription_user_excluded(self):
969 # With the subscriber logged in, he is not included in the results.
970 bug = self._makeBugWithNoSubscribers()
971
972=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
973--- lib/lp/bugs/javascript/subscribers_list.js 2011-06-27 14:23:58 +0000
974+++ lib/lp/bugs/javascript/subscribers_list.js 2011-06-30 22:44:42 +0000
975@@ -325,7 +325,8 @@
976
977 function on_success() {
978 loader.subscribers_list.stopSubscriberActivity(subscriber, true);
979- loader._addUnsubscribeLinkIfTeamMember(subscriber);
980+ loader.subscribers_list.addUnsubscribeAction(
981+ subscriber, loader._getUnsubscribeCallback());
982 }
983 function on_failure(t_id, response) {
984 loader.subscribers_list.stopSubscriberActivity(
985@@ -347,44 +348,6 @@
986 };
987
988 /**
989- * Add unsubscribe link for a team if the currently logged in user
990- * is member of the team.
991- *
992- * @method _addUnsubscribeLinkIfTeamMember
993- * @param team {Object} A person object as returned via API.
994- */
995-BugSubscribersLoader.prototype
996-._addUnsubscribeLinkIfTeamMember = function(team) {
997- var loader = this;
998- function on_success(members) {
999- var team_member = false;
1000- var i;
1001- for (i=0; i<members.entries.length; i++) {
1002- if (members.entries[i].get('member_link') ===
1003- Y.lp.client.get_absolute_uri(LP.links.me)) {
1004- team_member = true;
1005- break;
1006- }
1007- }
1008- if (team_member === true) {
1009- // Add unsubscribe action for the team member.
1010- loader.subscribers_list.addUnsubscribeAction(
1011- team, loader._getUnsubscribeCallback());
1012- }
1013- }
1014-
1015- if (Y.Lang.isString(LP.links.me) && team.is_team) {
1016- var config = {
1017- on: { success: on_success }
1018- };
1019-
1020- var members_link = team.members_details_collection_link;
1021- this.lp_client.get(members_link, config);
1022- }
1023-};
1024-
1025-
1026-/**
1027 * Manages entire subscribers' list for a single bug.
1028 *
1029 * If the passed in container_box is not present, or if there are multiple
1030
1031=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
1032--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-28 16:26:34 +0000
1033+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-30 22:44:42 +0000
1034@@ -2002,8 +2002,8 @@
1035
1036 test_subscribeSomeoneElse_success: function() {
1037 // When subscribing someone else succeeds, stopSubscriberActivity
1038- // is called indicating success, and _addUnsubscribeLinkIfTeamMember
1039- // is called to add unsubscribe-link when needed.
1040+ // is called indicating success, and addUnsubscribeAction
1041+ // is called with the correct parameters.
1042
1043 var subscriber = { name: "user", self_link: "/~user" };
1044
1045@@ -2011,17 +2011,29 @@
1046 var loader = setUpLoader(this.root);
1047 loader.subscribers_list.addSubscriber(subscriber, "Maybe");
1048
1049- // Mock-up addUnsubscribeLinkIfTeamMember method to
1050+ // Mock-up addUnsubscribeAction method to
1051 // ensure it's called with the right parameters.
1052- var subscriber_link_added = false;
1053- var old_addLink =
1054- module.BugSubscribersLoader
1055- .prototype._addUnsubscribeLinkIfTeamMember;
1056- module.BugSubscribersLoader.prototype
1057- ._addUnsubscribeLinkIfTeamMember =
1058- function(my_subscriber) {
1059+ // We need to stub the _getUnsubscribeCallback result so we can check
1060+ // the unsubscribe_callback.
1061+ var unsubscribe_callback = function() {};
1062+ // Save old methods for restoring later.
1063+ var old_getUnsub = module.BugSubscribersLoader.prototype
1064+ ._getUnsubscribeCallback;
1065+
1066+ // Make _getUnsubscribeCallback return the new callback.
1067+ module.BugSubscribersLoader.prototype._getUnsubscribeCallback =
1068+ function() {
1069+ return unsubscribe_callback;
1070+ };
1071+
1072+ var unsubscribe_link_added = false;
1073+ var old_unsubscribe_action =
1074+ module.SubscribersList.prototype.addUnsubscribeAction;
1075+ module.SubscribersList.prototype.addUnsubscribeAction =
1076+ function(my_subscriber, callback) {
1077 Y.Assert.areSame(subscriber, my_subscriber);
1078- subscriber_link_added = true;
1079+ Y.Assert.areEqual(unsubscribe_callback, callback);
1080+ unsubscribe_link_added = true;
1081 };
1082
1083 // Mock-up stopSubscriberActivity to ensure it's called.
1084@@ -2051,12 +2063,14 @@
1085
1086 loader._subscribeSomeoneElse(person);
1087
1088- Y.Assert.isTrue(subscriber_link_added);
1089+ Y.Assert.isTrue(unsubscribe_link_added);
1090 Y.Assert.isFalse(activity_on);
1091
1092 // Restore original methods.
1093- module.BugSubscribersLoader.prototype.addUnsubscribeLinkIfTeamMember =
1094- old_addLink;
1095+ module.SubscribersList.prototype.addUnsubscribeAction =
1096+ old_unsubscribe_action;
1097+ module.BugSubscribersLoader.prototype._getUnsubscribeCallback =
1098+ old_getUnsub;
1099 module.SubscribersList.prototype.stopSubscriberActivity =
1100 old_indicate;
1101
1102
1103=== modified file 'lib/lp/bugs/stories/bug-privacy/05-set-bug-private-as-admin.txt'
1104--- lib/lp/bugs/stories/bug-privacy/05-set-bug-private-as-admin.txt 2011-06-16 13:50:58 +0000
1105+++ lib/lp/bugs/stories/bug-privacy/05-set-bug-private-as-admin.txt 2011-06-30 22:44:42 +0000
1106@@ -15,7 +15,7 @@
1107 ... "http://launchpad.dev/bugs/2/+bug-portlet-subscribers-details")
1108
1109 >>> print_direct_subscribers(browser.contents)
1110- Steve Alexander
1111+ Steve Alexander (Unsubscribe)
1112 >>> print_also_notified(browser.contents)
1113 Also notified:
1114 Sample Person
1115@@ -38,8 +38,8 @@
1116 >>> browser.open(
1117 ... "http://launchpad.dev/bugs/2/+bug-portlet-subscribers-details")
1118 >>> print_direct_subscribers(browser.contents)
1119- Sample Person
1120- Steve Alexander
1121+ Sample Person (Unsubscribe)
1122+ Steve Alexander (Unsubscribe)
1123 Ubuntu Team (Unsubscribe)
1124
1125 >>> print_also_notified(browser.contents)
1126
1127=== modified file 'lib/lp/bugs/stories/bugs/bug-add-subscriber.txt'
1128--- lib/lp/bugs/stories/bugs/bug-add-subscriber.txt 2011-06-16 13:50:58 +0000
1129+++ lib/lp/bugs/stories/bugs/bug-add-subscriber.txt 2011-06-30 22:44:42 +0000
1130@@ -67,7 +67,7 @@
1131 ... 'http://bugs.launchpad.dev/bugs/1/'
1132 ... '+bug-portlet-subscribers-details')
1133 >>> print_direct_subscribers(user_browser.contents)
1134- David Allouche
1135+ David Allouche (Unsubscribe)
1136 Sample Person
1137 Steve Alexander
1138
1139@@ -110,8 +110,8 @@
1140 ... 'http://bugs.launchpad.dev/bugs/1/'
1141 ... '+bug-portlet-subscribers-details')
1142 >>> print_direct_subscribers(user_browser.contents)
1143- David Allouche
1144- Landscape Developers
1145+ David Allouche (Unsubscribe)
1146+ Landscape Developers (Unsubscribe)
1147 Sample Person
1148 Steve Alexander
1149
1150@@ -144,11 +144,11 @@
1151 ... 'http://bugs.launchpad.dev/bugs/1/'
1152 ... '+bug-portlet-subscribers-details')
1153 >>> print_direct_subscribers(foobar_browser.contents)
1154- David Allouche
1155- Landscape Developers
1156+ David Allouche (Unsubscribe)
1157+ Landscape Developers (Unsubscribe)
1158 Private Team (Unsubscribe)
1159- Sample Person
1160- Steve Alexander
1161+ Sample Person (Unsubscribe)
1162+ Steve Alexander (Unsubscribe)
1163
1164 Someone not in the team will not see the private team in the
1165 subscribers list.
1166@@ -157,8 +157,8 @@
1167 ... 'http://bugs.launchpad.dev/bugs/1/'
1168 ... '+bug-portlet-subscribers-details')
1169 >>> print_direct_subscribers(user_browser.contents)
1170- David Allouche
1171- Landscape Developers
1172+ David Allouche (Unsubscribe)
1173+ Landscape Developers (Unsubscribe)
1174 Sample Person
1175 Steve Alexander
1176