Ian Booth has proposed merging 
lp:~wallyworld/launchpad/sharing-details-delete3-966641 into lp:launchpad with 
lp:~wallyworld/launchpad/sharing-details-delete2-966641 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #966641 in Launchpad itself: "Delete button on sharing details page does 
nothing"
  https://bugs.launchpad.net/launchpad/+bug/966641

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/sharing-details-delete3-966641/+merge/100736

== 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

-- 
https://code.launchpad.net/~wallyworld/launchpad/sharing-details-delete3-966641/+merge/100736
Your team Launchpad code reviewers is requested to review the proposed merge of 
lp:~wallyworld/launchpad/sharing-details-delete3-966641 into lp:launchpad.
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/browser/pillar.py	2012-04-05 03:42:22 +0000
@@ -43,10 +43,6 @@
     )
 from lp.bugs.interfaces.bug import IBug
 from lp.code.interfaces.branch import IBranch
-from lp.registry.interfaces.accesspolicy import (
-    IAccessPolicyGrantFlatSource,
-    IAccessPolicySource,
-    )
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
     )
@@ -85,11 +81,6 @@
         person = getUtility(IPersonSet).getByName(name)
         if person is None:
             return None
-        policies = getUtility(IAccessPolicySource).findByPillar([self.context])
-        source = getUtility(IAccessPolicyGrantFlatSource)
-        artifacts = source.findArtifactsByGrantee(person, policies)
-        if artifacts.is_empty():
-            return None
         return PillarPerson.create(self.context, person)
 
 

=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py	2012-04-05 03:42:22 +0000
@@ -16,7 +16,6 @@
     Raises,
     )
 from zope.component import getUtility
-from zope.publisher.interfaces import NotFound
 from zope.security.interfaces import Unauthorized
 from zope.traversing.browser.absoluteurl import absoluteURL
 
@@ -103,17 +102,19 @@
             browser = self.getUserBrowser(user=self.owner, url=url)
             self.assertEqual(expected, browser.title)
 
-    def test_not_found_without_sharing(self):
-        # If there is no sharing between pillar and person, NotFound is the
-        # result.
+    def test_no_sharing_message(self):
+        # If there is no sharing between pillar and person, a suitable message
+        # is displayed.
         with FeatureFixture(DETAILS_ENABLED_FLAG):
             # We have to do some fun url hacking to force the traversal a user
             # encounters.
             pillarperson = self.getPillarPerson(with_sharing=False)
             url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
                 pillarperson.pillar.name, pillarperson.person.name)
-            browser = self.getUserBrowser(user=self.owner)
-            self.assertRaises(NotFound, browser.open, url)
+            browser = self.getUserBrowser(user=self.owner, url=url)
+            self.assertIn(
+                'There are no shared bugs or branches.',
+                browser.contents)
 
     def test_init_without_feature_flag(self):
         # We need a feature flag to enable the view.

=== modified file 'lib/lp/registry/javascript/sharing/sharingdetails.js'
--- lib/lp/registry/javascript/sharing/sharingdetails.js	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/javascript/sharing/sharingdetails.js	2012-04-05 03:42:22 +0000
@@ -23,7 +23,18 @@
     SharingDetailsTable.superclass.constructor.apply(this, arguments);
 }
 
+var clone_data = function(value) {
+    if (!Y.Lang.isArray(value)) {
+        return value;
+    }
+    return Y.JSON.parse(Y.JSON.stringify(value));
+};
+
 SharingDetailsTable.ATTRS = {
+    // The duration for various animations eg row deletion.
+    anim_duration: {
+        value: 1
+    },
     // The node holding the details table.
     details_table_body: {
         getter: function() {
@@ -43,11 +54,17 @@
     },
 
     bugs: {
-        value: []
+        value: [],
+        // We clone the data passed in so external modifications do not
+        // interfere.
+        setter: clone_data
     },
 
     branches: {
-        value: []
+        value: [],
+        // We clone the data passed in so external modifications do not
+        // interfere.
+        setter: clone_data
     },
 
     write_enabled: {
@@ -79,6 +96,9 @@
     renderUI: function() {
         var branch_data = this.get('branches');
         var bug_data = this.get('bugs');
+        if (bug_data.length === 0 && branch_data.length === 0) {
+            return;
+        }
         var partials = {
             branch: this.get('branch_details_row_template'),
             bug: this.get('bug_details_row_template')
@@ -124,6 +144,106 @@
         }, 'span[id^=remove-] a');
      },
 
+    /**
+     * The the artifact with id exists in the model, return it.
+     * @param artifact_id
+     * @param id_property
+     * @param model
+     * @return {*}
+     * @private
+     */
+    _get_artifact_from_model: function(artifact_id, id_property, model) {
+        var artifact = null;
+        Y.Array.some(model, function(item) {
+            if (item[id_property] === artifact_id) {
+                artifact = item;
+                return true;
+            }
+            return false;
+        });
+        return artifact;
+    },
+
+    syncUI: function() {
+        // Examine the widget's data model and delete and artifacts which have
+        // been removed.
+        var existing_bugs = this.get('bugs');
+        var existing_branches = this.get('branches');
+        var model_bugs = LP.cache.bugs;
+        var model_branches = LP.cache.branches;
+        var deleted_bugs = [];
+        var deleted_branches = [];
+        var self = this;
+        Y.Array.each(existing_bugs, function(bug) {
+            var model_bug =
+                self._get_artifact_from_model(
+                    bug.bug_id, 'bug_id', model_bugs);
+            if (!Y.Lang.isValue(model_bug)) {
+                deleted_bugs.push(bug);
+            }
+        });
+        Y.Array.each(existing_branches, function(branch) {
+            var model_branch =
+                self._get_artifact_from_model(
+                    branch.branch_id, 'branch_id', model_branches);
+            if (!Y.Lang.isValue(model_branch)) {
+                deleted_branches.push(branch);
+            }
+        });
+        if (deleted_bugs.length > 0 || deleted_branches.length > 0) {
+            this.delete_artifacts(
+                deleted_bugs, deleted_branches,
+                model_bugs.length === 0 && model_branches.length === 0);
+        }
+        this.set('bugs', model_bugs);
+        this.set('branches', model_branches);
+    },
+
+    // Delete the specified sharees from the table.
+    delete_artifacts: function(bugs, branches, all_rows_deleted) {
+        var deleted_row_selectors = [];
+        var details_table_body = this.get('details_table_body');
+        Y.Array.each(bugs, function(bug) {
+            var selector = 'tr[id=shared-bug-' + bug.bug_id + ']';
+            var table_row = details_table_body.one(selector);
+            if (Y.Lang.isValue(table_row)) {
+                deleted_row_selectors.push(selector);
+            }
+        });
+        Y.Array.each(branches, function(branch) {
+            var selector = 'tr[id=shared-branch-' + branch.branch_id + ']';
+            var table_row = details_table_body.one(selector);
+            if (Y.Lang.isValue(table_row)) {
+                deleted_row_selectors.push(selector);
+            }
+        });
+        if (deleted_row_selectors.length === 0) {
+            return;
+        }
+        var rows_to_delete = details_table_body.all(
+            deleted_row_selectors.join(','));
+        var delete_rows = function() {
+            rows_to_delete.remove(true);
+            if (all_rows_deleted === true) {
+                details_table_body
+                    .appendChild('<tr></tr>')
+                    .appendChild('<td colspan="4"></td>')
+                    .setContent("There are no shared bugs or branches.");
+            }
+        };
+        var anim_duration = this.get('anim_duration');
+        if (anim_duration === 0 ) {
+            delete_rows();
+            return;
+        }
+        var anim = Y.lp.anim.green_flash(
+            {node: rows_to_delete, duration:anim_duration});
+        anim.on('end', function() {
+            delete_rows();
+        });
+        anim.run();
+    },
+
     _table_body_template: function() {
         return [
         '<tbody id="sharing-table-body">',
@@ -188,5 +308,5 @@
 namespace.SharingDetailsTable = SharingDetailsTable;
 
 }, "0.1", { "requires": [
-    'node', 'event', 'lp.mustache'
+    'node', 'event', 'json', 'lp.mustache', 'lp.anim'
 ] });

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.html	2012-04-05 03:42:22 +0000
@@ -29,6 +29,10 @@
           src="../../../../../../build/js/lp/app/lp.js"></script>
       <script type="text/javascript"
           src="../../../../../../build/js/lp/app/mustache.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/extras/extras.js"></script>
 
       <!-- The module under test. -->
       <script type="text/javascript" src="../sharingdetails.js"></script>

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2012-04-05 03:42:22 +0000
@@ -55,6 +55,7 @@
                 overrides = {};
             }
             var config = Y.merge({
+                anim_duration: 0,
                 person_name: 'Fred',
                 bugs: window.LP.cache.bugs,
                 branches: window.LP.cache.branches,
@@ -137,6 +138,23 @@
             Y.Assert.isTrue(event_fired);
         },
 
+        // Model changes are rendered correctly when syncUI() is called.
+        test_syncUI: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            // We manipulate the cached model data - delete a bug.
+            var bugs = window.LP.cache.bugs;
+            // Delete the first bug.
+            bugs.splice(0, 1);
+            this.details_widget.syncUI();
+            // Check the results.
+            var bug_row_selector = '#sharing-table-body tr[id=shared-bug-2]';
+            Y.Assert.isNull(Y.one(bug_row_selector));
+            var branch_row_selector =
+                '#sharing-table-body tr[id=shared-branch-2]';
+            Y.Assert.isNotNull(Y.one(branch_row_selector));
+        },
+
         // When the branch revoke link is clicked, the correct event is
         // published.
         test_branch_revoke_click: function() {
@@ -164,6 +182,41 @@
                 Y.one('#sharing-table-body span[id=remove-branch-2] a');
             delete_link_to_click.simulate('click');
             Y.Assert.isTrue(event_fired);
+        },
+
+        // The delete_artifacts call removes the specified bugs from the table.
+        test_delete_bugs: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var row_selector = '#sharing-table-body tr[id=shared-bug-2]';
+            Y.Assert.isNotNull(Y.one(row_selector));
+            this.details_widget.delete_artifacts(
+                [window.LP.cache.bugs[0]], [], false);
+            Y.Assert.isNull(Y.one(row_selector));
+        },
+
+        // The delete_artifacts call removes the specified branches from the
+        // table.
+        test_delete_branches: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var row_selector = '#sharing-table-body tr[id=shared-branch-2]';
+            Y.Assert.isNotNull(Y.one(row_selector));
+            this.details_widget.delete_artifacts(
+                [], [window.LP.cache.branches[0]], false);
+            Y.Assert.isNull(Y.one(row_selector));
+        },
+
+        // When all artifacts are deleted, a suitable message is displayed.
+        test_delete_all_artifacts: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            this.details_widget.delete_artifacts(
+                [window.LP.cache.bugs[0]],
+                [window.LP.cache.branches[0]], true);
+            Y.Assert.areEqual(
+                'There are no shared bugs or branches.',
+                Y.one('#sharing-table-body tr').get('text'));
         }
     }));
 

=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
--- lib/lp/registry/templates/pillar-sharing-details.pt	2012-04-05 03:42:21 +0000
+++ lib/lp/registry/templates/pillar-sharing-details.pt	2012-04-05 03:42:22 +0000
@@ -50,7 +50,13 @@
           </th>
         </tr>
       </thead>
-      <tbody id="sharing-table-body"></tbody>
+      <tbody id="sharing-table-body">
+          <tr>
+              <td colspan="4">
+                  There are no shared bugs or branches.
+              </td>
+          </tr>
+      </tbody>
     </table>
 
   </div>

_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp

Reply via email to