← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/shared_with_blueprints_1078751-2 into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/shared_with_blueprints_1078751-2 into lp:launchpad.

Commit message:
Add specifications to the list of shared items in the person details sharing UI.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1078751 in Launchpad itself: ""Information shared with..." page does not list shared blueprints"
  https://bugs.launchpad.net/launchpad/+bug/1078751

For more details, see:
https://code.launchpad.net/~rharding/launchpad/shared_with_blueprints_1078751-2/+merge/137410

= Summary =

Per the bug, we need to show access grants to specifications as well as bugs
and branches.


== Pre Implementation ==

N/A


== Implementation Notes ==

Note that first up, I'm not sure if the UI should say specification or
blueprints. Honestly, now that I've got it working I'm going to guess
blueprints.

The table for this view is all JS generated. There were parts of the code that
already existed. For instance, getSharedArtifacts was already set to return
specifications as well. It just wasn't wired into the LP.cache data for the JS
view to use.

So we first started out adding that data into the LP.cache and the template
data.

The text strings around the "bugs or branches" all needed to be updated to
"bugs, branches, or specifications". Should this be specs or blueprints? I
need to confirm.

While there was some work with specifications done, it wasn't added to the API
call for the revokeAccessGrants call. So that got updated. However, the
argument already existed and was passed to the internals of the method. The
API just never pulled/recognized it.

The JS was a bit hard to follow. I didn't rewrite it, but I did rework it so
that things were alphabetized and the class was created from Y.Base.extend()
vs the manual constructing of the class long form. Sorry for the diff noise,
but it was hard to figure out this JS code at first and reorg was very
helpful.

The new bits are around adding a third type everywhere. There's the new
spec_details_row_template, updated partial call in table_body_template, added
.each block for specifications in delete_artifacts, and updated code in the
renderUI/initialize methods to include specs.

Note that one thing was that there were a couple of if/else meant to catch
bugs vs branches. I changed these to full switch() statements so that it could
catch all three. I made sure that the default throws an exception so that if
things get broken it's more apparent.

Most of the tests are actually in the test_sharingdetailsview. There's a ton
of cross over in the code between sharingdetails and sharingdetailsview, but
I tried to avoid too much refactoring in the interest of time and diff size.

-- 
https://code.launchpad.net/~rharding/launchpad/shared_with_blueprints_1078751-2/+merge/137410
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/shared_with_blueprints_1078751-2 into lp:launchpad.
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2012-09-17 16:19:36 +0000
+++ lib/lp/registry/browser/pillar.py	2012-12-02 03:32:19 +0000
@@ -406,6 +406,8 @@
         request = get_current_web_service_request()
         branch_data = self._build_branch_template_data(self.branches, request)
         bug_data = self._build_bug_template_data(self.bugtasks, request)
+        spec_data = self._build_specification_template_data(
+            self.specifications, request)
         grantee_data = {
             'displayname': self.person.displayname,
             'self_link': absoluteURL(self.person, request)
@@ -417,6 +419,7 @@
         cache.objects['pillar'] = pillar_data
         cache.objects['bugs'] = bug_data
         cache.objects['branches'] = branch_data
+        cache.objects['specifications'] = spec_data
 
     def _loadSharedArtifacts(self):
         # As a concrete can by linked via more than one policy, we use sets to
@@ -427,6 +430,18 @@
         bug_ids = set([bugtask.bug.id for bugtask in self.bugtasks])
         self.shared_bugs_count = len(bug_ids)
         self.shared_branches_count = len(self.branches)
+        self.shared_specifications_count = len(self.specifications)
+
+    def _build_specification_template_data(self, specs, request):
+        spec_data = []
+        for spec in specs:
+            spec_data.append(dict(
+                self_link=absoluteURL(spec, request),
+                web_link=canonical_url(spec, path_only_if_possible=True),
+                name=spec.name,
+                id=spec.id,
+                information_type=spec.information_type.title))
+        return spec_data
 
     def _build_branch_template_data(self, branches, request):
         branch_data = []

=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py	2012-11-15 22:22:38 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py	2012-12-02 03:32:19 +0000
@@ -161,7 +161,7 @@
             pillarperson.pillar.name, pillarperson.person.name)
         browser = self.getUserBrowser(user=self.owner, url=url)
         self.assertIn(
-            'There are no shared bugs or branches.', browser.contents)
+            'There are no shared bugs, branches, or specifications.', browser.contents)
 
     def test_init_works(self):
         # The view works with a feature flag.

=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py	2012-11-12 23:03:24 +0000
+++ lib/lp/registry/interfaces/sharingservice.py	2012-12-02 03:32:19 +0000
@@ -308,7 +308,9 @@
         bugs=List(
             Reference(schema=IBug), title=_('Bugs'), required=False),
         branches=List(
-            Reference(schema=IBranch), title=_('Branches'), required=False))
+            Reference(schema=IBranch), title=_('Branches'), required=False),
+        specifications=List(
+            Reference(schema=ISpecification), title=_('Specifications'), required=False))
     @operation_for_version('devel')
     def revokeAccessGrants(pillar, grantee, user, branches=None, bugs=None,
                            specifications=None):

=== modified file 'lib/lp/registry/javascript/sharing/sharingdetails.js'
--- lib/lp/registry/javascript/sharing/sharingdetails.js	2012-07-27 20:35:57 +0000
+++ lib/lp/registry/javascript/sharing/sharingdetails.js	2012-12-02 03:32:19 +0000
@@ -8,20 +8,7 @@
 
 YUI.add('lp.registry.sharing.sharingdetails', function(Y) {
 
-var namespace = Y.namespace('lp.registry.sharing.sharingdetails');
-
-var
-    NAME = "sharingDetailsTable",
-    // Events
-    REMOVE_GRANT = 'removeGrant';
-
-/*
- * Sharing details table widget.
- * This widget displays the details of a specific person's shared artifacts.
- */
-function SharingDetailsTable(config) {
-    SharingDetailsTable.superclass.constructor.apply(this, arguments);
-}
+var ns = Y.namespace('lp.registry.sharing.sharingdetails');
 
 var clone_data = function(value) {
     if (!Y.Lang.isArray(value)) {
@@ -30,53 +17,206 @@
     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() {
-            return Y.one('#sharing-table-body');
-        }
-    },
-    table_body_template: {
-        value: null
-    },
-
-    bug_details_row_template: {
-        value: null
-    },
-
-    branch_details_row_template: {
-        value: null
-    },
-
-    bugs: {
-        value: [],
-        // We clone the data passed in so external modifications do not
-        // interfere.
-        setter: clone_data
-    },
-
-    branches: {
-        value: [],
-        // We clone the data passed in so external modifications do not
-        // interfere.
-        setter: clone_data
-    },
-
-    write_enabled: {
-        value: false
-    },
-
-    person_name: {
-        value: null
-    }
-};
-
-Y.extend(SharingDetailsTable, Y.Widget, {
+/*
+ * Sharing details table widget.
+ * This widget displays the details of a specific person's shared artifacts.
+ */
+ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
+
+    _bug_details_row_template: function() {
+        return [
+        '<tr id="shared-bug-{{ bug_id }}">',
+        '    <td>',
+        '        <span class="sortkey">{{bug_id}}</span>',
+        '        <span class="sprite bug-{{bug_importance}}">{{bug_id}}</span>',
+        '        <a href="{{web_link}}">{{bug_summary}}</a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-bug-{{ bug_id }}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare bug {{bug_id}} with {{displayname}}"',
+        '        data-self_link="{{self_link}}" data-name="Bug {{bug_id}}"',
+        '        data-type="bug">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
+    _branch_details_row_template: function() {
+        return [
+        '<tr id="shared-branch-{{ branch_id }}">',
+        '    <td>',
+        '        <span class="sortkey">sorttable_branchsortkey</span>',
+        '        <a class="sprite branch" href="{{web_link}}">',
+        '            {{branch_name}}',
+        '        </a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-branch-{{branch_id}}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare branch {{branch_name}} with {{displayname}}"',
+        '        data-self_link="{{self_link}}" data-name="{{branch_name}}"',
+        '        data-type="branch">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
+    /**
+     * If 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;
+    },
+
+    _spec_details_row_template: function() {
+        return [
+        '<tr id="shared-spec-{{id}}">',
+        '    <td>',
+        '        <span class="sortkey">{{id}}</span>',
+        '        <a class="sprite branch" href="{{web_link}}">',
+        '            {{name}}',
+        '        </a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-spec-{{id}}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare branch {{name}} with {{displayname}}"',
+        '        data-self_link="{{self_link}}" data-name="{{name}}"',
+        '        data-type="spec">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
+    _table_body_template: function() {
+        return [
+        '<tbody id="sharing-table-body">',
+        '{{#bugs}}',
+        '{{> bug}}',
+        '{{/bugs}}',
+        '{{#branches}}',
+        '{{> branch}}',
+        '{{/branches}}',
+        '{{#specifications}}',
+        '{{> spec}}',
+        '{{/specifications}}',
+        '</tbody>'
+        ].join(' ');
+    },
+
+    _update_editable_status: function() {
+        var details_table_body = this.get('details_table_body');
+        if (!this.get('write_enabled')) {
+            details_table_body.all('.sprite.remove').each(function(node) {
+                node.addClass('hidden');
+            });
+        }
+    },
+
+    branch_sort_key: function(cell, default_func) {
+        // Generate the sort key for branches.
+        return 'BRANCH:' + default_func(cell, true);
+    },
+
+    // Delete the specified grantees from the table.
+    delete_artifacts: function(bugs, branches, specifications,
+                               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);
+            }
+        });
+        Y.Array.each(specifications, function(spec) {
+            var selector = 'tr[id=shared-spec-' + spec.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);
+            // Now check if there are any rows left.
+            var left = details_table_body.all('tr');
+            if (left.isEmpty()) {
+                details_table_body
+                    .appendChild('<tr></tr>')
+                    .appendChild('<td colspan="3"></td>')
+                    .setContent("There are no shared bugs, branches, or specifications.");
+            }
+        };
+        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();
+    },
+
+    // An error occurred performing an operation on an artifact.
+    display_error: function(artifact_id, artifact_type, error_msg) {
+        var details_table_body = this.get('details_table_body');
+        var selector = Y.Lang.substitute(
+            'tr[id=shared-{artifact_type}-{artifact_id}]', {
+                artifact_type: artifact_type,
+                artifact_id: artifact_id});
+        var artifact_row = details_table_body.one(selector);
+        Y.lp.app.errors.display_error(artifact_row, error_msg);
+    },
 
     initializer: function(config) {
         this.set(
@@ -88,26 +228,39 @@
             this._branch_details_row_template());
 
         this.set(
+            'spec_details_row_template',
+            this._spec_details_row_template());
+
+        this.set(
             'table_body_template',
             this._table_body_template());
-        this.publish(REMOVE_GRANT);
+        this.publish(ns.SharingDetailsTable.REMOVE_GRANT);
     },
 
     renderUI: function() {
-        var branch_data = this.get('branches');
-        var bug_data = this.get('bugs');
-        if (bug_data.length === 0 && branch_data.length === 0) {
+        // Load the data
+        var branches = this.get('branches');
+        var bugs = this.get('bugs');
+        var specs = this.get('specifications');
+
+        if (bugs.length === 0 && branches.length === 0 &&
+            specs.length === 0 ) {
             return;
         }
         var partials = {
             branch: this.get('branch_details_row_template'),
-            bug: this.get('bug_details_row_template')
+            bug: this.get('bug_details_row_template'),
+            spec: this.get('spec_details_row_template')
         };
         var template = this.get('table_body_template');
         var html = Y.lp.mustache.to_html(
             template,
-            {branches: branch_data, bugs: bug_data,
-            displayname: this.get('person_name')},
+            {
+                branches: branches,
+                bugs: bugs,
+                specifications: specs,
+                displayname: this.get('person_name')
+            },
             partials);
 
         var details_table_body = this.get('details_table_body');
@@ -116,16 +269,7 @@
         this._update_editable_status();
     },
 
-    _update_editable_status: function() {
-        var details_table_body = this.get('details_table_body');
-        if (!this.get('write_enabled')) {
-            details_table_body.all('.sprite.remove').each(function(node) {
-                node.addClass('hidden');
-            });
-        }
-    },
-
-     bindUI: function() {
+    bindUI: function() {
         // Bind the delete links.
         if (!this.get('write_enabled')) {
             return;
@@ -139,34 +283,10 @@
             var artifact_name = delete_link.getAttribute('data-name');
             var artifact_type = delete_link.getAttribute('data-type');
             self.fire(
-                REMOVE_GRANT, delete_link, artifact_uri, artifact_name,
+                ns.SharingDetailsTable.REMOVE_GRANT,
+                delete_link, artifact_uri, artifact_name,
                 artifact_type);
         }, 'span[id^=remove-] a');
-     },
-
-    /**
-     * If 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;
-    },
-
-    branch_sort_key: function(cell, default_func) {
-        // Generate the sort key for branches.
-        return 'BRANCH:' + default_func(cell, true);
     },
 
     syncUI: function() {
@@ -174,10 +294,14 @@
         // been removed.
         var existing_bugs = this.get('bugs');
         var existing_branches = this.get('branches');
+        var existing_specifications = this.get('specifications');
         var model_bugs = LP.cache.bugs;
         var model_branches = LP.cache.branches;
+        var model_specifications = LP.cache.specifications;
         var deleted_bugs = [];
         var deleted_branches = [];
+        var deleted_specifications = [];
+
         var self = this;
         Y.Array.each(existing_bugs, function(bug) {
             var model_bug =
@@ -195,145 +319,100 @@
                 deleted_branches.push(branch);
             }
         });
-        if (deleted_bugs.length > 0 || deleted_branches.length > 0) {
+
+        Y.Array.each(existing_specifications, function(spec) {
+            var model_specification = self._get_artifact_from_model(
+                    spec.id, 'id', model_specifications);
+            if (!Y.Lang.isValue(model_specification)) {
+                deleted_specifications.push(spec);
+            }
+        });
+
+        if (deleted_bugs.length > 0 || deleted_branches.length > 0 ||
+            deleted_specifications.length > 0) {
             this.delete_artifacts(
-                deleted_bugs, deleted_branches,
-                model_bugs.length === 0 && model_branches.length === 0);
+                deleted_bugs, deleted_branches, deleted_specifications,
+                model_bugs.length === 0 && model_branches.length === 0 && deleted_specifications.length === 0);
         }
+
         this.set('bugs', model_bugs);
         this.set('branches', model_branches);
+        this.set('specifications', model_specifications);
+
         Y.lp.app.sorttable.SortTable.registerSortKeyFunction(
             'branchsortkey', this.branch_sort_key);
         Y.lp.app.sorttable.SortTable.init(true);
-    },
-
-    // Delete the specified grantees 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="3"></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();
-    },
-
-    // An error occurred performing an operation on an artifact.
-    display_error: function(artifact_id, artifact_type, error_msg) {
-        var details_table_body = this.get('details_table_body');
-        var selector = Y.Lang.substitute(
-            'tr[id=shared-{artifact_type}-{artifact_id}]', {
-                artifact_type: artifact_type,
-                artifact_id: artifact_id});
-        var artifact_row = details_table_body.one(selector);
-        Y.lp.app.errors.display_error(artifact_row, error_msg);
-    },
-
-    _table_body_template: function() {
-        return [
-        '<tbody id="sharing-table-body">',
-        '{{#bugs}}',
-        '{{> bug}}',
-        '{{/bugs}}',
-        '{{#branches}}',
-        '{{> branch}}',
-        '{{/branches}}',
-        '</tbody>'
-        ].join(' ');
-    },
-
-    _bug_details_row_template: function() {
-        return [
-        '<tr id="shared-bug-{{ bug_id }}">',
-        '    <td>',
-        '        <span class="sortkey">{{bug_id}}</span>',
-        '        <span class="sprite bug-{{bug_importance}}">{{bug_id}}</span>',
-        '        <a href="{{web_link}}">{{bug_summary}}</a>',
-        '    </td>',
-        '    <td class="action-icons nowrap">',
-        '    <span id="remove-bug-{{ bug_id }}">',
-        '    <a class="sprite remove action-icon" href="#"',
-        '        title="Unshare bug {{bug_id}} with {{displayname}}"',
-        '        data-self_link="{{self_link}}" data-name="Bug {{bug_id}}"',
-        '        data-type="bug">Remove</a>',
-        '    </span>',
-        '    </td>',
-        '   <td>',
-        '   <span class="information_type">',
-        '  {{information_type}}',
-        '   </span>',
-        '   </td>',
-        '</tr>'
-        ].join(' ');
-    },
-
-    _branch_details_row_template: function() {
-        return [
-        '<tr id="shared-branch-{{ branch_id }}">',
-        '    <td>',
-        '        <span class="sortkey">sorttable_branchsortkey</span>',
-        '        <a class="sprite branch" href="{{web_link}}">',
-        '            {{branch_name}}',
-        '        </a>',
-        '    </td>',
-        '    <td class="action-icons nowrap">',
-        '    <span id="remove-branch-{{branch_id}}">',
-        '    <a class="sprite remove action-icon" href="#"',
-        '        title="Unshare branch {{branch_name}} with {{displayname}}"',
-        '        data-self_link="{{self_link}}" data-name="{{branch_name}}"',
-        '        data-type="branch">Remove</a>',
-        '    </span>',
-        '    </td>',
-        '   <td>',
-        '   <span class="information_type">',
-        '  {{information_type}}',
-        '   </span>',
-        '   </td>',
-        '</tr>'
-        ].join(' ');
-    }
+    }
+
+    },  {
+        REMOVE_GRANT: 'removeGrant',
+        ATTRS: {
+            // The duration for various animations eg row deletion.
+            anim_duration: {
+                value: 1
+            },
+
+            bug_details_row_template: {
+                value: null
+            },
+
+            branch_details_row_template: {
+                value: null
+            },
+
+            spec_details_row_template: {
+                value: null
+            },
+
+            branches: {
+                value: [],
+                // We clone the data passed in so external modifications do not
+                // interfere.
+                setter: clone_data
+            },
+
+            bugs: {
+                value: [],
+                // We clone the data passed in so external modifications do not
+                // interfere.
+                setter: clone_data
+            },
+
+            // The node holding the details table.
+            details_table_body: {
+                getter: function() {
+                    return Y.one('#sharing-table-body');
+                }
+            },
+
+            person_name: {
+                value: null
+            },
+
+            /**
+             * The list of specifications a user has a grant to.
+             * @attribute specifications
+             * @default []
+             * @type Array
+             *
+             */
+            specifications: {
+                value: [],
+                setter: clone_data
+            },
+
+            table_body_template: {
+                value: null
+            },
+
+            write_enabled: {
+                value: false
+            }
+        }
+    }
+);
+
+} , "0.1", {
+    requires: ['base', 'node', 'event', 'json', 'lp.mustache', 'lp.anim',
+               'lp.app.sorttable', 'lp.app.errors']
 });
-
-SharingDetailsTable.NAME = NAME;
-SharingDetailsTable.REMOVE_GRANT = REMOVE_GRANT;
-
-namespace.SharingDetailsTable = SharingDetailsTable;
-
-}, "0.1", { "requires": [
-    'node', 'event', 'json', 'lp.mustache', 'lp.anim',
-    'lp.app.sorttable', 'lp.app.errors'
-] });

=== modified file 'lib/lp/registry/javascript/sharing/sharingdetailsview.js'
--- lib/lp/registry/javascript/sharing/sharingdetailsview.js	2012-08-30 02:40:04 +0000
+++ lib/lp/registry/javascript/sharing/sharingdetailsview.js	2012-12-02 03:32:19 +0000
@@ -32,6 +32,7 @@
             bugs: LP.cache.bugs,
             branches: LP.cache.branches,
             person_name: LP.cache.grantee.displayname,
+            specifications: LP.cache.specifications,
             write_enabled: true
         });
         this.set('sharing_details_table', details_table);
@@ -57,21 +58,35 @@
     // A common error handler for XHR operations.
     _error_handler: function(artifact_uri, artifact_type) {
         var artifact_id;
-        if (artifact_type === 'bug') {
-            Y.Array.some(LP.cache.bugs, function(bug) {
-                if (bug.self_link === artifact_uri) {
-                    artifact_id = bug.bug_id;
-                    return true;
-                }
-            });
-        } else {
-            Y.Array.some(LP.cache.branches, function(branch) {
-                if (branch.self_link === artifact_uri) {
-                    artifact_id = branch.branch_id;
-                    return true;
-                }
-            });
+        switch (artifact_type) {
+            case 'bug':
+                Y.Array.some(LP.cache.bugs, function(bug) {
+                    if (bug.self_link === artifact_uri) {
+                        artifact_id = bug.bug_id;
+                        return true;
+                    }
+                });
+                break;
+            case 'branch':
+                Y.Array.some(LP.cache.branches, function(branch) {
+                    if (branch.self_link === artifact_uri) {
+                        artifact_id = branch.branch_id;
+                        return true;
+                    }
+                });
+                break;
+            case 'spec':
+                Y.Array.some(LP.cache.specifications, function(spec) {
+                    if (spec.self_link === artifact_uri) {
+                        artifact_id = spec.id;
+                        return true;
+                    }
+                });
+                break;
+            default:
+                throw('Invalid artifact type.' + artifact_type);
         }
+
         var error_handler = new Y.lp.client.ErrorHandler();
         var self = this;
         error_handler.showError = function(error_msg) {
@@ -166,6 +181,14 @@
                 return true;
             }
         });
+        var spec_data = LP.cache.specifications;
+        Y.Array.some(spec_data, function(spec, index) {
+            if (spec.self_link === artifact_uri) {
+                spec_data.splice(index, 1);
+                self.syncUI();
+                return true;
+            }
+        });
     },
 
     /**
@@ -176,15 +199,25 @@
      * @param artifact_type
      */
     perform_remove_grant: function(delete_link, artifact_uri, artifact_type) {
+        var self = this;
         var error_handler = this._error_handler(artifact_uri, artifact_type);
         var bugs = [];
         var branches = [];
-        if (artifact_type === 'bug') {
-            bugs = [artifact_uri];
-        } else {
-            branches = [artifact_uri];
+        var specifications = [];
+        switch (artifact_type) {
+            case 'bug':
+                bugs = [artifact_uri];
+                break;
+            case 'branch':
+                branches = [artifact_uri];
+                break;
+            case 'spec':
+                specifications = [artifact_uri];
+                break;
+            default:
+                throw('Invalid artifact type.' + artifact_type);
         }
-        var self = this;
+
         var y_config =  {
             on: {
                 start: Y.bind(
@@ -199,7 +232,8 @@
                 pillar: LP.cache.pillar.self_link,
                 grantee: LP.cache.grantee.self_link,
                 bugs: bugs,
-                branches: branches
+                branches: branches,
+                specifications: specifications
             }
         };
         this.get('lp_client').named_post(
@@ -210,8 +244,9 @@
 SharingDetailsView.NAME = 'sharingDetailsView';
 namespace.SharingDetailsView = SharingDetailsView;
 
-}, "0.1", { "requires": [
-    'node', 'selector-css3', 'lp.client', 'lp.mustache',
-    'lp.registry.sharing.sharingdetails', 'lp.app.confirmationoverlay'
-    ]});
+}, "0.1", {
+    requires: ['node', 'selector-css3', 'lp.client', 'lp.mustache',
+               'lp.registry.sharing.sharingdetails',
+               'lp.app.confirmationoverlay']
+});
 

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2012-10-26 10:00:20 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2012-12-02 03:32:19 +0000
@@ -41,7 +41,6 @@
             var sharing_table = Y.Node.create(
                     Y.one('#sharing-table-template').getContent());
             this.fixture.appendChild(sharing_table);
-
         },
 
         tearDown: function () {
@@ -73,6 +72,15 @@
                 "lp.registry.sharing.sharingdetails module");
         },
 
+        test_constants_is_set: function () {
+            Y.Assert.areEqual(
+                'sharingDetailsTable',
+                sharing_details.SharingDetailsTable.NAME);
+            Y.Assert.areEqual(
+                'removeGrant',
+                sharing_details.SharingDetailsTable.REMOVE_GRANT);
+        },
+
         test_widget_can_be_instantiated: function() {
             this.details_widget = this._create_Widget();
             Y.Assert.isInstanceOf(
@@ -245,7 +253,7 @@
                 [window.LP.cache.bugs[0]],
                 [window.LP.cache.branches[0]], true);
             Y.Assert.areEqual(
-                'There are no shared bugs or branches.',
+                'There are no shared bugs, branches, or specifications.',
                 Y.one('#sharing-table-body tr').get('text'));
         },
 

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js	2012-10-26 10:00:20 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js	2012-12-02 03:32:19 +0000
@@ -30,13 +30,22 @@
                             branch_name:'lp:~someone/+junk/somebranch'
                         }
                     ],
+                    specifications: [
+                        {
+                            id: 2,
+                            information_type: "Proprietary",
+                            name: "big-project",
+                            self_link: "api/devel/obsolete-junk/+spec/big-project",
+                            web_link: "/obsolete-junk/+spec/big-project"
+                        }
+                    ],
                     grantee: {
                         displayname: 'Fred Bloggs',
                         self_link: '~fred'
                     },
                     pillar: {
                         self_link: '/pillar'
-                    },
+                    }
                 }
             };
             this.fixture = Y.one('#fixture');
@@ -122,6 +131,28 @@
             Y.Assert.isTrue(confirmRemove_called);
         },
 
+        // Clicking a spec remove link calls the confirm_grant_removal
+        // method with the correct parameters.
+        test_remove_spec_grant_click: function() {
+            this.view = this._create_Widget();
+            this.view.render();
+            var confirmRemove_called = false;
+            this.view.confirm_grant_removal = function(
+                    delete_link, artifact_uri, artifact_name, artifact_type) {
+                Y.Assert.areEqual(
+                    'api/devel/obsolete-junk/+spec/big-project',
+                    artifact_uri);
+                Y.Assert.areEqual('big-project', artifact_name);
+                Y.Assert.areEqual('spec', artifact_type);
+                Y.Assert.areEqual(delete_link_to_click, delete_link);
+                confirmRemove_called = true;
+            };
+            var delete_link_to_click =
+                Y.one('#sharing-table-body span[id=remove-spec-2] a');
+            delete_link_to_click.simulate('click');
+            Y.Assert.isTrue(confirmRemove_called);
+        },
+
         //Test the behaviour of the removal confirmation dialog.
         _test_confirm_grant_removal: function(click_ok) {
             this.view = this._create_Widget();
@@ -274,6 +305,24 @@
             });
         },
 
+        // The remove specification grant callback updates the model and syncs
+        // the UI.
+        test_remove_spec_grant_success: function() {
+            this.view = this._create_Widget({anim_duration: 0});
+            this.view.render();
+            var syncUI_called = false;
+            this.view.syncUI = function() {
+                syncUI_called = true;
+            };
+            this.view.remove_grant_success(
+                'api/devel/obsolete-junk/+spec/big-project');
+            Y.Assert.isTrue(syncUI_called);
+
+            // Make sure the are no more specs in the cache.
+            Y.Assert.areEqual(0, LP.cache.specifications.length,
+                'All specs are removed from the cache.');
+        },
+
         // XHR calls display errors correctly.
         _assert_error_displayed_on_failure: function(
                 bug_or_branch, invoke_operation) {
@@ -328,6 +377,19 @@
             this._assert_error_displayed_on_failure('branch', invoke_remove);
         },
 
+        // The perform_remove_grant method handles errors correctly with
+        // specifications.
+        test_perform_remove_spec_error: function() {
+            var invoke_remove = function(view) {
+                var delete_link =
+                    Y.one('#sharing-table-body span[id=remove-spec-2] a');
+                    view.perform_remove_grant(
+                        delete_link,
+                        'api/devel/obsolete-junk/+spec/big-project',
+                        'spec');
+            };
+            this._assert_error_displayed_on_failure('spec', invoke_remove);
+        },
         // Test that syncUI works as expected.
         test_syncUI: function() {
             this.view = this._create_Widget();

=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
--- lib/lp/registry/templates/pillar-sharing-details.pt	2012-07-27 19:57:28 +0000
+++ lib/lp/registry/templates/pillar-sharing-details.pt	2012-12-02 03:32:19 +0000
@@ -24,11 +24,14 @@
 
 <body>
   <div metal:fill-slot="main">
+
     <div id="observer-summary">
       <p>
-      <tal:bugs replace="view/shared_bugs_count">0</tal:bugs> bugs and
-      <tal:branches replace="view/shared_branches_count">0</tal:branches>
-      branches shared with <tal:name replace="view/person/displayname">
+      <tal:bugs replace="view/shared_bugs_count">0</tal:bugs> bugs,
+      <tal:branches replace="view/shared_branches_count">0</tal:branches> branches,
+      and  <tal:specifications
+      replace="view/shared_specifications_count">0</tal:specifications>
+      specifications shared with <tal:name replace="view/person/displayname">
       grantee</tal:name>.<br />
 
       <tal:is-team condition="view/person/is_team">
@@ -45,7 +48,7 @@
       <thead>
         <tr>
           <th colspan="2" width="">
-            Subscribed Bug Report or Branch
+            Subscribed Bug Report, Branch, or Specification
           </th>
           <th>
             Information Type
@@ -55,7 +58,7 @@
       <tbody id="sharing-table-body">
           <tr>
               <td colspan="3">
-                  There are no shared bugs or branches.
+                  There are no shared bugs, branches, or specifications.
               </td>
           </tr>
       </tbody>


Follow ups