Merge lp:~wallyworld/launchpad/sharing-details-delete3-966641 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15067
Proposed branch: lp:~wallyworld/launchpad/sharing-details-delete3-966641
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/sharing-details-delete2-966641
Diff against target: 342 lines (+194/-19)
6 files modified
lib/lp/registry/browser/pillar.py (+0/-9)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+7/-6)
lib/lp/registry/javascript/sharing/sharingdetails.js (+123/-3)
lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html (+4/-0)
lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js (+53/-0)
lib/lp/registry/templates/pillar-sharing-details.pt (+7/-1)
To merge this branch: bzr merge lp:~wallyworld/launchpad/sharing-details-delete3-966641
Reviewer Review Type Date Requested Status
Richard Harding (community) Abstain
Curtis Hovey (community) code Approve
Review via email: mp+100736@code.launchpad.net

Commit message

Wire up the sharing details ui so that when a pillar artifact is deleted, the ui is updated.

Description of the change

== Implementation ==

Complete the work to allow grants on bugs/branches to be revoked for a user. This branch wires up the ui so that when a successful XHR call is made to revoke access, the sharing details table is updated.

A change was made to the original sharing detail implementation. A NotFound error was raised if the Sharing Details page was requested but there were no shared artifacts. This would only have been applicable if the user url hacked since the link to the details page is not rendered on the Sharing Information page if there are no shared items. However, consider the case where a user goes to the Sharing Details page and there are shared artifacts. They delete all of them. This branch includes javascript code to display a suitable message if the XHR call to revoke removes the last shared artifact - "There are no shared bugs or branches.". If the user hits refresh, they would now get an error and this doesn't seem the best thing to do. Instead now, this branch ensures that they simply see the Sharing Details page with the above message.

== Tests ==

Add yui tests for the new sharingdetails widget functionality.
Modify the PillarSharingDetailsMixin to test for the new behaviour of showing a user friendly message if there are no shared items.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/pillar.py
  lib/lp/registry/browser/tests/test_pillar_sharing.py
  lib/lp/registry/javascript/sharing/sharingdetails.js
  lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html
  lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
  lib/lp/registry/templates/pillar-sharing-details.pt

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)
Revision history for this message
Richard Harding (rharding) :
review: Abstain

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/pillar.py'
2--- lib/lp/registry/browser/pillar.py 2012-04-05 03:52:27 +0000
3+++ lib/lp/registry/browser/pillar.py 2012-04-05 03:52:28 +0000
4@@ -43,10 +43,6 @@
5 )
6 from lp.bugs.interfaces.bug import IBug
7 from lp.code.interfaces.branch import IBranch
8-from lp.registry.interfaces.accesspolicy import (
9- IAccessPolicyGrantFlatSource,
10- IAccessPolicySource,
11- )
12 from lp.registry.interfaces.distributionsourcepackage import (
13 IDistributionSourcePackage,
14 )
15@@ -85,11 +81,6 @@
16 person = getUtility(IPersonSet).getByName(name)
17 if person is None:
18 return None
19- policies = getUtility(IAccessPolicySource).findByPillar([self.context])
20- source = getUtility(IAccessPolicyGrantFlatSource)
21- artifacts = source.findArtifactsByGrantee(person, policies)
22- if artifacts.is_empty():
23- return None
24 return PillarPerson.create(self.context, person)
25
26
27
28=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
29--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-05 03:52:27 +0000
30+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-05 03:52:28 +0000
31@@ -16,7 +16,6 @@
32 Raises,
33 )
34 from zope.component import getUtility
35-from zope.publisher.interfaces import NotFound
36 from zope.security.interfaces import Unauthorized
37 from zope.traversing.browser.absoluteurl import absoluteURL
38
39@@ -103,17 +102,19 @@
40 browser = self.getUserBrowser(user=self.owner, url=url)
41 self.assertEqual(expected, browser.title)
42
43- def test_not_found_without_sharing(self):
44- # If there is no sharing between pillar and person, NotFound is the
45- # result.
46+ def test_no_sharing_message(self):
47+ # If there is no sharing between pillar and person, a suitable message
48+ # is displayed.
49 with FeatureFixture(DETAILS_ENABLED_FLAG):
50 # We have to do some fun url hacking to force the traversal a user
51 # encounters.
52 pillarperson = self.getPillarPerson(with_sharing=False)
53 url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
54 pillarperson.pillar.name, pillarperson.person.name)
55- browser = self.getUserBrowser(user=self.owner)
56- self.assertRaises(NotFound, browser.open, url)
57+ browser = self.getUserBrowser(user=self.owner, url=url)
58+ self.assertIn(
59+ 'There are no shared bugs or branches.',
60+ browser.contents)
61
62 def test_init_without_feature_flag(self):
63 # We need a feature flag to enable the view.
64
65=== modified file 'lib/lp/registry/javascript/sharing/sharingdetails.js'
66--- lib/lp/registry/javascript/sharing/sharingdetails.js 2012-04-05 03:52:27 +0000
67+++ lib/lp/registry/javascript/sharing/sharingdetails.js 2012-04-05 03:52:28 +0000
68@@ -23,7 +23,18 @@
69 SharingDetailsTable.superclass.constructor.apply(this, arguments);
70 }
71
72+var clone_data = function(value) {
73+ if (!Y.Lang.isArray(value)) {
74+ return value;
75+ }
76+ return Y.JSON.parse(Y.JSON.stringify(value));
77+};
78+
79 SharingDetailsTable.ATTRS = {
80+ // The duration for various animations eg row deletion.
81+ anim_duration: {
82+ value: 1
83+ },
84 // The node holding the details table.
85 details_table_body: {
86 getter: function() {
87@@ -43,11 +54,17 @@
88 },
89
90 bugs: {
91- value: []
92+ value: [],
93+ // We clone the data passed in so external modifications do not
94+ // interfere.
95+ setter: clone_data
96 },
97
98 branches: {
99- value: []
100+ value: [],
101+ // We clone the data passed in so external modifications do not
102+ // interfere.
103+ setter: clone_data
104 },
105
106 write_enabled: {
107@@ -79,6 +96,9 @@
108 renderUI: function() {
109 var branch_data = this.get('branches');
110 var bug_data = this.get('bugs');
111+ if (bug_data.length === 0 && branch_data.length === 0) {
112+ return;
113+ }
114 var partials = {
115 branch: this.get('branch_details_row_template'),
116 bug: this.get('bug_details_row_template')
117@@ -124,6 +144,106 @@
118 }, 'span[id^=remove-] a');
119 },
120
121+ /**
122+ * If the artifact with id exists in the model, return it.
123+ * @param artifact_id
124+ * @param id_property
125+ * @param model
126+ * @return {*}
127+ * @private
128+ */
129+ _get_artifact_from_model: function(artifact_id, id_property, model) {
130+ var artifact = null;
131+ Y.Array.some(model, function(item) {
132+ if (item[id_property] === artifact_id) {
133+ artifact = item;
134+ return true;
135+ }
136+ return false;
137+ });
138+ return artifact;
139+ },
140+
141+ syncUI: function() {
142+ // Examine the widget's data model and delete any artifacts which have
143+ // been removed.
144+ var existing_bugs = this.get('bugs');
145+ var existing_branches = this.get('branches');
146+ var model_bugs = LP.cache.bugs;
147+ var model_branches = LP.cache.branches;
148+ var deleted_bugs = [];
149+ var deleted_branches = [];
150+ var self = this;
151+ Y.Array.each(existing_bugs, function(bug) {
152+ var model_bug =
153+ self._get_artifact_from_model(
154+ bug.bug_id, 'bug_id', model_bugs);
155+ if (!Y.Lang.isValue(model_bug)) {
156+ deleted_bugs.push(bug);
157+ }
158+ });
159+ Y.Array.each(existing_branches, function(branch) {
160+ var model_branch =
161+ self._get_artifact_from_model(
162+ branch.branch_id, 'branch_id', model_branches);
163+ if (!Y.Lang.isValue(model_branch)) {
164+ deleted_branches.push(branch);
165+ }
166+ });
167+ if (deleted_bugs.length > 0 || deleted_branches.length > 0) {
168+ this.delete_artifacts(
169+ deleted_bugs, deleted_branches,
170+ model_bugs.length === 0 && model_branches.length === 0);
171+ }
172+ this.set('bugs', model_bugs);
173+ this.set('branches', model_branches);
174+ },
175+
176+ // Delete the specified sharees from the table.
177+ delete_artifacts: function(bugs, branches, all_rows_deleted) {
178+ var deleted_row_selectors = [];
179+ var details_table_body = this.get('details_table_body');
180+ Y.Array.each(bugs, function(bug) {
181+ var selector = 'tr[id=shared-bug-' + bug.bug_id + ']';
182+ var table_row = details_table_body.one(selector);
183+ if (Y.Lang.isValue(table_row)) {
184+ deleted_row_selectors.push(selector);
185+ }
186+ });
187+ Y.Array.each(branches, function(branch) {
188+ var selector = 'tr[id=shared-branch-' + branch.branch_id + ']';
189+ var table_row = details_table_body.one(selector);
190+ if (Y.Lang.isValue(table_row)) {
191+ deleted_row_selectors.push(selector);
192+ }
193+ });
194+ if (deleted_row_selectors.length === 0) {
195+ return;
196+ }
197+ var rows_to_delete = details_table_body.all(
198+ deleted_row_selectors.join(','));
199+ var delete_rows = function() {
200+ rows_to_delete.remove(true);
201+ if (all_rows_deleted === true) {
202+ details_table_body
203+ .appendChild('<tr></tr>')
204+ .appendChild('<td colspan="4"></td>')
205+ .setContent("There are no shared bugs or branches.");
206+ }
207+ };
208+ var anim_duration = this.get('anim_duration');
209+ if (anim_duration === 0 ) {
210+ delete_rows();
211+ return;
212+ }
213+ var anim = Y.lp.anim.green_flash(
214+ {node: rows_to_delete, duration:anim_duration});
215+ anim.on('end', function() {
216+ delete_rows();
217+ });
218+ anim.run();
219+ },
220+
221 _table_body_template: function() {
222 return [
223 '<tbody id="sharing-table-body">',
224@@ -188,5 +308,5 @@
225 namespace.SharingDetailsTable = SharingDetailsTable;
226
227 }, "0.1", { "requires": [
228- 'node', 'event', 'lp.mustache'
229+ 'node', 'event', 'json', 'lp.mustache', 'lp.anim'
230 ] });
231
232=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html'
233--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html 2012-04-05 03:52:27 +0000
234+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html 2012-04-05 03:52:28 +0000
235@@ -29,6 +29,10 @@
236 src="../../../../../../build/js/lp/app/lp.js"></script>
237 <script type="text/javascript"
238 src="../../../../../../build/js/lp/app/mustache.js"></script>
239+ <script type="text/javascript"
240+ src="../../../../../../build/js/lp/app/anim/anim.js"></script>
241+ <script type="text/javascript"
242+ src="../../../../../../build/js/lp/app/extras/extras.js"></script>
243
244 <!-- The module under test. -->
245 <script type="text/javascript" src="../sharingdetails.js"></script>
246
247=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js'
248--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js 2012-04-05 03:52:27 +0000
249+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js 2012-04-05 03:52:28 +0000
250@@ -55,6 +55,7 @@
251 overrides = {};
252 }
253 var config = Y.merge({
254+ anim_duration: 0,
255 person_name: 'Fred',
256 bugs: window.LP.cache.bugs,
257 branches: window.LP.cache.branches,
258@@ -137,6 +138,23 @@
259 Y.Assert.isTrue(event_fired);
260 },
261
262+ // Model changes are rendered correctly when syncUI() is called.
263+ test_syncUI: function() {
264+ this.details_widget = this._create_Widget();
265+ this.details_widget.render();
266+ // We manipulate the cached model data - delete a bug.
267+ var bugs = window.LP.cache.bugs;
268+ // Delete the first bug.
269+ bugs.splice(0, 1);
270+ this.details_widget.syncUI();
271+ // Check the results.
272+ var bug_row_selector = '#sharing-table-body tr[id=shared-bug-2]';
273+ Y.Assert.isNull(Y.one(bug_row_selector));
274+ var branch_row_selector =
275+ '#sharing-table-body tr[id=shared-branch-2]';
276+ Y.Assert.isNotNull(Y.one(branch_row_selector));
277+ },
278+
279 // When the branch revoke link is clicked, the correct event is
280 // published.
281 test_branch_revoke_click: function() {
282@@ -164,6 +182,41 @@
283 Y.one('#sharing-table-body span[id=remove-branch-2] a');
284 delete_link_to_click.simulate('click');
285 Y.Assert.isTrue(event_fired);
286+ },
287+
288+ // The delete_artifacts call removes the specified bugs from the table.
289+ test_delete_bugs: function() {
290+ this.details_widget = this._create_Widget();
291+ this.details_widget.render();
292+ var row_selector = '#sharing-table-body tr[id=shared-bug-2]';
293+ Y.Assert.isNotNull(Y.one(row_selector));
294+ this.details_widget.delete_artifacts(
295+ [window.LP.cache.bugs[0]], [], false);
296+ Y.Assert.isNull(Y.one(row_selector));
297+ },
298+
299+ // The delete_artifacts call removes the specified branches from the
300+ // table.
301+ test_delete_branches: function() {
302+ this.details_widget = this._create_Widget();
303+ this.details_widget.render();
304+ var row_selector = '#sharing-table-body tr[id=shared-branch-2]';
305+ Y.Assert.isNotNull(Y.one(row_selector));
306+ this.details_widget.delete_artifacts(
307+ [], [window.LP.cache.branches[0]], false);
308+ Y.Assert.isNull(Y.one(row_selector));
309+ },
310+
311+ // When all artifacts are deleted, a suitable message is displayed.
312+ test_delete_all_artifacts: function() {
313+ this.details_widget = this._create_Widget();
314+ this.details_widget.render();
315+ this.details_widget.delete_artifacts(
316+ [window.LP.cache.bugs[0]],
317+ [window.LP.cache.branches[0]], true);
318+ Y.Assert.areEqual(
319+ 'There are no shared bugs or branches.',
320+ Y.one('#sharing-table-body tr').get('text'));
321 }
322 }));
323
324
325=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
326--- lib/lp/registry/templates/pillar-sharing-details.pt 2012-04-05 03:52:27 +0000
327+++ lib/lp/registry/templates/pillar-sharing-details.pt 2012-04-05 03:52:28 +0000
328@@ -50,7 +50,13 @@
329 </th>
330 </tr>
331 </thead>
332- <tbody id="sharing-table-body"></tbody>
333+ <tbody id="sharing-table-body">
334+ <tr>
335+ <td colspan="4">
336+ There are no shared bugs or branches.
337+ </td>
338+ </tr>
339+ </tbody>
340 </table>
341
342 </div>