Merge ~pappacena/launchpad:ocirecipe-sharing-lists into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: b4a3aecaec628183a2cd92c066894997bd3168be
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:ocirecipe-sharing-lists
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:ocirecipe-edit-info-type-ui
Diff against target: 921 lines (+532/-39)
8 files modified
lib/lp/registry/browser/pillar.py (+27/-0)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+56/-12)
lib/lp/registry/javascript/sharing/sharingdetails.js (+140/-7)
lib/lp/registry/javascript/sharing/sharingdetailsview.js (+45/-1)
lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js (+27/-2)
lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js (+210/-1)
lib/lp/registry/services/sharingservice.py (+1/-1)
lib/lp/registry/templates/pillar-sharing-details.pt (+26/-15)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+400059@code.launchpad.net

Commit message

Showing snaps and ocirecipes as shareable artifacts on +sharing pages

To post a comment you must log in.
e8dcad4... by Thiago F. Pappacena

Merge branch 'ocirecipe-edit-info-type-ui' into ocirecipe-sharing-lists

8245248... by Thiago F. Pappacena

Merge branch 'ocirecipe-edit-info-type-ui' into ocirecipe-sharing-lists

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
75fc124... by Thiago F. Pappacena

Removing icon and standardizing names

8eedbb4... by Thiago F. Pappacena

Merge branch 'master' into ocirecipe-sharing-lists

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Replied all comments. I might worth another quick review round, or at least a validation of the screenshots.

3e3d9aa... by Thiago F. Pappacena

Fixing broken test

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
b4a3aec... by Thiago F. Pappacena

Fixing texts and bug on team members count

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed text and style fixes, and a pre-existing bug on team members count.

Revision history for this message
Colin Watson (cjwatson) wrote :

Much better, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py
2index c5bfd3f..739b56f 100644
3--- a/lib/lp/registry/browser/pillar.py
4+++ b/lib/lp/registry/browser/pillar.py
5@@ -422,6 +422,9 @@ class PillarPersonSharingView(LaunchpadView):
6 bug_data = self._build_bug_template_data(self.bugtasks, request)
7 spec_data = self._build_specification_template_data(
8 self.specifications, request)
9+ snap_data = self._build_ocirecipe_template_data(self.snaps, request)
10+ ocirecipe_data = self._build_ocirecipe_template_data(
11+ self.ocirecipes, request)
12 grantee_data = {
13 'displayname': self.person.displayname,
14 'self_link': absoluteURL(self.person, request)
15@@ -435,6 +438,8 @@ class PillarPersonSharingView(LaunchpadView):
16 cache.objects['branches'] = branch_data
17 cache.objects['gitrepositories'] = gitrepository_data
18 cache.objects['specifications'] = spec_data
19+ cache.objects['snaps'] = snap_data
20+ cache.objects['ocirecipes'] = ocirecipe_data
21
22 def _loadSharedArtifacts(self):
23 # As a concrete can by linked via more than one policy, we use sets to
24@@ -504,3 +509,25 @@ class PillarPersonSharingView(LaunchpadView):
25 bug_importance=importance,
26 information_type=information_type))
27 return bug_data
28+
29+ def _build_ocirecipe_template_data(self, oci_recipes, request):
30+ recipe_data = []
31+ for recipe in oci_recipes:
32+ recipe_data.append(dict(
33+ self_link=absoluteURL(recipe, request),
34+ web_link=canonical_url(recipe, path_only_if_possible=True),
35+ name=recipe.name,
36+ id=recipe.id,
37+ information_type=recipe.information_type.title))
38+ return recipe_data
39+
40+ def _build_snap_template_data(self, snaps, request):
41+ snap_data = []
42+ for snap in snaps:
43+ snap_data.append(dict(
44+ self_link=absoluteURL(snap, request),
45+ web_link=canonical_url(snap, path_only_if_possible=True),
46+ name=snap.name,
47+ id=snap.id,
48+ information_type=snap.information_type.title))
49+ return snap_data
50diff --git a/lib/lp/registry/browser/tests/test_pillar_sharing.py b/lib/lp/registry/browser/tests/test_pillar_sharing.py
51index 432c473..082276d 100644
52--- a/lib/lp/registry/browser/tests/test_pillar_sharing.py
53+++ b/lib/lp/registry/browser/tests/test_pillar_sharing.py
54@@ -25,6 +25,7 @@ from lp.registry.enums import (
55 BranchSharingPolicy,
56 BugSharingPolicy,
57 PersonVisibility,
58+ TeamMembershipPolicy,
59 )
60 from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantFlatSource
61 from lp.registry.model.pillar import PillarPerson
62@@ -179,8 +180,9 @@ class PillarSharingDetailsMixin:
63 pillarperson.pillar.name, pillarperson.person.name)
64 browser = self.getUserBrowser(user=self.owner, url=url)
65 self.assertIn(
66- 'There are no shared bugs, Bazaar branches, Git repositories, or '
67- 'blueprints.', normalize_whitespace(browser.contents))
68+ 'There are no shared bugs, Bazaar branches, Git repositories, '
69+ 'snap recipes, OCI recipes or blueprints.',
70+ normalize_whitespace(browser.contents))
71
72 def test_init_works(self):
73 # The view works with a feature flag.
74@@ -386,23 +388,69 @@ class PillarSharingViewTestMixin:
75 team_name,
76 [grantee['name'] for grantee in cache.objects['grantee_data']])
77
78- def test_pillar_person_sharing(self):
79+ def test_pillar_person_sharing_with_team(self):
80 self.useFixture(FeatureFixture({
81 SNAP_PRIVATE_FEATURE_FLAG: 'on',
82 OCI_RECIPE_ALLOW_CREATE: 'on'}))
83- totals = {"oci_recipes": 1, "snaps": 0}
84+ team = self.factory.makeTeam(
85+ membership_policy=TeamMembershipPolicy.MODERATED)
86+ # Add 4 members to the team, so we should have the team owner + 4
87+ # other members with access to the artifacts.
88+ for i in range(4):
89+ self.factory.makePerson(member_of=[team])
90+
91 items = [
92 self.factory.makeOCIRecipe(
93 owner=self.owner, registrant=self.owner,
94 information_type=InformationType.USERDATA,
95 oci_project=self.factory.makeOCIProject(pillar=self.pillar))]
96+ expected_text = """
97+ 5 team members can view these artifacts.
98+ Shared with %s:
99+ 1 OCI recipes
100+ """ % team.displayname
101+
102 if self.pillar_type == 'product':
103- totals["snaps"] = 1
104 items.append(self.factory.makeSnap(
105 information_type=InformationType.USERDATA,
106 owner=self.owner, registrant=self.owner, project=self.pillar))
107+ expected_text += "\n1 snap recipes"
108+
109+ with person_logged_in(self.owner):
110+ for item in items:
111+ item.subscribe(team, self.owner)
112
113+ pillarperson = PillarPerson(self.pillar, team)
114+ url = 'http://launchpad.test/%s/+sharing/%s' % (
115+ pillarperson.pillar.name, pillarperson.person.name)
116+ browser = self.getUserBrowser(user=self.owner, url=url)
117+ content = extract_text(
118+ find_tag_by_id(browser.contents, "observer-summary"))
119+
120+ self.assertTextMatchesExpressionIgnoreWhitespace(
121+ expected_text, content)
122+
123+ def test_pillar_person_sharing(self):
124+ self.useFixture(FeatureFixture({
125+ SNAP_PRIVATE_FEATURE_FLAG: 'on',
126+ OCI_RECIPE_ALLOW_CREATE: 'on'}))
127 person = self.factory.makePerson()
128+ items = [
129+ self.factory.makeOCIRecipe(
130+ owner=self.owner, registrant=self.owner,
131+ information_type=InformationType.USERDATA,
132+ oci_project=self.factory.makeOCIProject(pillar=self.pillar))]
133+ expected_text = """
134+ Shared with %s:
135+ 1 OCI recipes
136+ """ % person.displayname
137+
138+ if self.pillar_type == 'product':
139+ items.append(self.factory.makeSnap(
140+ information_type=InformationType.USERDATA,
141+ owner=self.owner, registrant=self.owner, project=self.pillar))
142+ expected_text += "\n1 snap recipes"
143+
144 with person_logged_in(self.owner):
145 for item in items:
146 item.subscribe(person, self.owner)
147@@ -413,13 +461,9 @@ class PillarSharingViewTestMixin:
148 browser = self.getUserBrowser(user=self.owner, url=url)
149 content = extract_text(
150 find_tag_by_id(browser.contents, "observer-summary"))
151- self.assertTextMatchesExpressionIgnoreWhitespace("""
152- 0 bugs,
153- 0 Bazaar branches,
154- 0 Git repositories,
155- %(snaps)s snaps,
156- and 0 blueprints shared
157- """ % totals, content)
158+
159+ self.assertTextMatchesExpressionIgnoreWhitespace(
160+ expected_text, content)
161
162
163 class TestProductSharingView(PillarSharingViewTestMixin,
164diff --git a/lib/lp/registry/javascript/sharing/sharingdetails.js b/lib/lp/registry/javascript/sharing/sharingdetails.js
165index 0ac4f32..9c4f002 100644
166--- a/lib/lp/registry/javascript/sharing/sharingdetails.js
167+++ b/lib/lp/registry/javascript/sharing/sharingdetails.js
168@@ -1,4 +1,4 @@
169-/* Copyright 2012-2015 Canonical Ltd. This software is licensed under the
170+/* Copyright 2012-2021 Canonical Ltd. This software is licensed under the
171 * GNU Affero General Public License version 3 (see the file LICENSE).
172 *
173 * Sharing details widget
174@@ -148,6 +148,58 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
175 ].join(' ');
176 },
177
178+ _snap_details_row_template: function() {
179+ return [
180+ '<tr id="shared-snap-{{id}}">',
181+ ' <td>',
182+ ' <span class="sortkey">{{id}}</span>',
183+ ' <a href="{{web_link}}">',
184+ ' {{name}}',
185+ ' </a>',
186+ ' </td>',
187+ ' <td class="action-icons nowrap">',
188+ ' <span id="remove-snap-{{id}}">',
189+ ' <a class="sprite remove action-icon" href="#"',
190+ ' title="Unshare Snap {{name}} with {{displayname}}"',
191+ ' data-self_link="{{self_link}}" data-name="{{name}}"',
192+ ' data-type="snap">Remove</a>',
193+ ' </span>',
194+ ' </td>',
195+ ' <td>',
196+ ' <span class="information_type">',
197+ ' {{information_type}}',
198+ ' </span>',
199+ ' </td>',
200+ '</tr>'
201+ ].join(' ');
202+ },
203+
204+ _ocirecipe_details_row_template: function() {
205+ return [
206+ '<tr id="shared-ocirecipe-{{id}}">',
207+ ' <td>',
208+ ' <span class="sortkey">{{id}}</span>',
209+ ' <a href="{{web_link}}">',
210+ ' {{name}}',
211+ ' </a>',
212+ ' </td>',
213+ ' <td class="action-icons nowrap">',
214+ ' <span id="remove-ocirecipe-{{id}}">',
215+ ' <a class="sprite remove action-icon" href="#"',
216+ ' title="Unshare OCI recipe {{name}} with {{displayname}}"',
217+ ' data-self_link="{{self_link}}" data-name="{{name}}"',
218+ ' data-type="ocirecipe">Remove</a>',
219+ ' </span>',
220+ ' </td>',
221+ ' <td>',
222+ ' <span class="information_type">',
223+ ' {{information_type}}',
224+ ' </span>',
225+ ' </td>',
226+ '</tr>'
227+ ].join(' ');
228+ },
229+
230 _table_body_template: function() {
231 return [
232 '<tbody id="sharing-table-body">',
233@@ -160,6 +212,12 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
234 '{{#gitrepositories}}',
235 '{{> gitrepository}}',
236 '{{/gitrepositories}}',
237+ '{{#snaps}}',
238+ '{{> snap}}',
239+ '{{/snaps}}',
240+ '{{#ocirecipes}}',
241+ '{{> ocirecipe}}',
242+ '{{/ocirecipes}}',
243 '{{#specifications}}',
244 '{{> spec}}',
245 '{{/specifications}}',
246@@ -183,7 +241,7 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
247
248 // Delete the specified grantees from the table.
249 delete_artifacts: function(bugs, branches, gitrepositories, specifications,
250- all_rows_deleted) {
251+ snaps, ocirecipes, all_rows_deleted) {
252 var deleted_row_selectors = [];
253 var details_table_body = this.get('details_table_body');
254 Y.Array.each(bugs, function(bug) {
255@@ -215,6 +273,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
256 deleted_row_selectors.push(selector);
257 }
258 });
259+ Y.Array.each(snaps, function(snap) {
260+ var selector = 'tr[id=shared-snap-' + snap.id + ']';
261+ var table_row = details_table_body.one(selector);
262+ if (Y.Lang.isValue(table_row)) {
263+ deleted_row_selectors.push(selector);
264+ }
265+ });
266+ Y.Array.each(ocirecipes, function(ocirecipe) {
267+ var selector = 'tr[id=shared-ocirecipe-' + ocirecipe.id + ']';
268+ var table_row = details_table_body.one(selector);
269+ if (Y.Lang.isValue(table_row)) {
270+ deleted_row_selectors.push(selector);
271+ }
272+ });
273
274 if (deleted_row_selectors.length === 0) {
275 return;
276@@ -232,7 +304,8 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
277 .appendChild('<td colspan="3"></td>')
278 .setContent(
279 "There are no shared bugs, Bazaar branches, " +
280- "Git repositories, or blueprints.");
281+ "Git repositories, snap recipes, OCI recipes or " +
282+ "blueprints.");
283 }
284 };
285 var anim_duration = this.get('anim_duration');
286@@ -275,6 +348,12 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
287 this.set(
288 'spec_details_row_template',
289 this._spec_details_row_template());
290+ this.set(
291+ 'snap_details_row_template',
292+ this._snap_details_row_template());
293+ this.set(
294+ 'ocirecipe_details_row_template',
295+ this._ocirecipe_details_row_template());
296
297 this.set(
298 'table_body_template',
299@@ -288,16 +367,21 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
300 var gitrepositories = this.get('gitrepositories');
301 var bugs = this.get('bugs');
302 var specs = this.get('specifications');
303+ var snaps = this.get('snaps');
304+ var ocirecipes = this.get('ocirecipes');
305
306 if (bugs.length === 0 && branches.length === 0 &&
307- gitrepositories.length === 0 && specs.length === 0 ) {
308+ gitrepositories.length === 0 && specs.length === 0 &&
309+ snaps.length === 0 && ocirecipes.length === 0) {
310 return;
311 }
312 var partials = {
313 branch: this.get('branch_details_row_template'),
314 gitrepository: this.get('gitrepository_details_row_template'),
315 bug: this.get('bug_details_row_template'),
316- spec: this.get('spec_details_row_template')
317+ spec: this.get('spec_details_row_template'),
318+ snap: this.get('snap_details_row_template'),
319+ ocirecipe: this.get('ocirecipe_details_row_template')
320 };
321 var template = this.get('table_body_template');
322 var html = Y.lp.mustache.to_html(
323@@ -307,6 +391,8 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
324 gitrepositories: gitrepositories,
325 bugs: bugs,
326 specifications: specs,
327+ snaps: snaps,
328+ ocirecipes: ocirecipes,
329 displayname: this.get('person_name')
330 },
331 partials);
332@@ -344,14 +430,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
333 var existing_branches = this.get('branches');
334 var existing_gitrepositories = this.get('gitrepositories');
335 var existing_specifications = this.get('specifications');
336+ var existing_snaps = this.get('snaps');
337+ var existing_ocirecipes = this.get('ocirecipes');
338 var model_bugs = LP.cache.bugs;
339 var model_branches = LP.cache.branches;
340 var model_gitrepositories = LP.cache.gitrepositories;
341 var model_specifications = LP.cache.specifications;
342+ var model_snaps = LP.cache.snaps;
343+ var model_ocirecipes = LP.cache.ocirecipes;
344 var deleted_bugs = [];
345 var deleted_branches = [];
346 var deleted_gitrepositories = [];
347 var deleted_specifications = [];
348+ var deleted_snaps = [];
349+ var deleted_ocirecipes = [];
350
351 var self = this;
352 Y.Array.each(existing_bugs, function(bug) {
353@@ -388,12 +480,29 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
354 }
355 });
356
357+ Y.Array.each(existing_snaps, function(snap) {
358+ var model_snap = self._get_artifact_from_model(
359+ snap.id, 'id', model_snaps);
360+ if (!Y.Lang.isValue(model_snap)) {
361+ deleted_snaps.push(snap);
362+ }
363+ });
364+
365+ Y.Array.each(existing_ocirecipes, function(ocirecipe) {
366+ var model_ocirecipe = self._get_artifact_from_model(
367+ ocirecipe.id, 'id', model_ocirecipes);
368+ if (!Y.Lang.isValue(model_ocirecipe)) {
369+ deleted_ocirecipes.push(ocirecipe);
370+ }
371+ });
372+
373 if (deleted_bugs.length > 0 || deleted_branches.length > 0 ||
374 deleted_gitrepositories.length > 0 ||
375- deleted_specifications.length > 0) {
376+ deleted_specifications.length > 0 || deleted_snaps.length > 0 ||
377+ deleted_ocirecipes.length > 0) {
378 this.delete_artifacts(
379 deleted_bugs, deleted_branches, deleted_gitrepositories,
380- deleted_specifications,
381+ deleted_specifications, deleted_snaps, deleted_ocirecipes,
382 model_bugs.length === 0 && model_branches.length === 0 &&
383 model_gitrepositories.length === 0 &&
384 deleted_specifications.length === 0);
385@@ -403,6 +512,8 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
386 this.set('branches', model_branches);
387 this.set('gitrepositories', model_gitrepositories);
388 this.set('specifications', model_specifications);
389+ this.set('snaps', model_snaps);
390+ this.set('ocirecipes', model_ocirecipes);
391
392 Y.lp.app.sorttable.SortTable.registerSortKeyFunction(
393 'branchsortkey', this.branch_sort_key);
394@@ -433,6 +544,14 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
395 value: null
396 },
397
398+ snap_details_row_template: {
399+ value: null
400+ },
401+
402+ ocirecipe_details_row_template: {
403+ value: null
404+ },
405+
406 branches: {
407 value: [],
408 // We clone the data passed in so external modifications do not
409@@ -454,6 +573,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
410 setter: clone_data
411 },
412
413+ snaps: {
414+ value: [],
415+ // We clone the data passed in so external modifications do not
416+ // interfere.
417+ setter: clone_data
418+ },
419+
420+ ocirecipes: {
421+ value: [],
422+ // We clone the data passed in so external modifications do not
423+ // interfere.
424+ setter: clone_data
425+ },
426+
427 // The node holding the details table.
428 details_table_body: {
429 getter: function() {
430diff --git a/lib/lp/registry/javascript/sharing/sharingdetailsview.js b/lib/lp/registry/javascript/sharing/sharingdetailsview.js
431index b41e9fe..d4197be 100644
432--- a/lib/lp/registry/javascript/sharing/sharingdetailsview.js
433+++ b/lib/lp/registry/javascript/sharing/sharingdetailsview.js
434@@ -1,4 +1,4 @@
435-/* Copyright 2012-2015 Canonical Ltd. This software is licensed under the
436+/* Copyright 2012-2021 Canonical Ltd. This software is licensed under the
437 * GNU Affero General Public License version 3 (see the file LICENSE).
438 *
439 * Disclosure infrastructure.
440@@ -32,6 +32,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
441 bugs: LP.cache.bugs,
442 branches: LP.cache.branches,
443 gitrepositories: LP.cache.gitrepositories,
444+ snaps: LP.cache.snaps,
445+ ocirecipes: LP.cache.ocirecipes,
446 person_name: LP.cache.grantee.displayname,
447 specifications: LP.cache.specifications,
448 write_enabled: true
449@@ -85,6 +87,22 @@ Y.extend(SharingDetailsView, Y.Widget, {
450 }
451 });
452 break;
453+ case 'snap':
454+ Y.Array.some(LP.cache.snaps, function(snap) {
455+ if (snap.self_link === artifact_uri) {
456+ artifact_id = snap.id;
457+ return true;
458+ }
459+ });
460+ break;
461+ case 'ocirecipe':
462+ Y.Array.some(LP.cache.ocirecipes, function(ocirecipe) {
463+ if (ocirecipe.self_link === artifact_uri) {
464+ artifact_id = ocirecipe.id;
465+ return true;
466+ }
467+ });
468+ break;
469 case 'spec':
470 Y.Array.some(LP.cache.specifications, function(spec) {
471 if (spec.self_link === artifact_uri) {
472@@ -207,6 +225,22 @@ Y.extend(SharingDetailsView, Y.Widget, {
473 return true;
474 }
475 });
476+ var snap_data = LP.cache.snaps;
477+ Y.Array.some(snap_data, function(snap, index) {
478+ if (snap.self_link === artifact_uri) {
479+ snap_data.splice(index, 1);
480+ self.syncUI();
481+ return true;
482+ }
483+ });
484+ var ocirecipe_data = LP.cache.ocirecipes;
485+ Y.Array.some(ocirecipe_data, function(ocirecipe, index) {
486+ if (ocirecipe.self_link === artifact_uri) {
487+ ocirecipe_data.splice(index, 1);
488+ self.syncUI();
489+ return true;
490+ }
491+ });
492 },
493
494 /**
495@@ -223,6 +257,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
496 var branches = [];
497 var gitrepositories = [];
498 var specifications = [];
499+ var snaps = [];
500+ var ocirecipes = [];
501 switch (artifact_type) {
502 case 'bug':
503 bugs = [artifact_uri];
504@@ -233,6 +269,12 @@ Y.extend(SharingDetailsView, Y.Widget, {
505 case 'gitrepository':
506 gitrepositories = [artifact_uri];
507 break;
508+ case 'snap':
509+ snaps = [artifact_uri];
510+ break;
511+ case 'ocirecipe':
512+ ocirecipes = [artifact_uri];
513+ break;
514 case 'spec':
515 specifications = [artifact_uri];
516 break;
517@@ -256,6 +298,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
518 bugs: bugs,
519 branches: branches,
520 gitrepositories: gitrepositories,
521+ snaps: snaps,
522+ ocirecipes: ocirecipes,
523 specifications: specifications
524 }
525 };
526diff --git a/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js b/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
527index 7b7d306..b78795e 100644
528--- a/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
529+++ b/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
530@@ -1,4 +1,4 @@
531-/* Copyright 2012-2015 Canonical Ltd. This software is licensed under the
532+/* Copyright 2012-2021 Canonical Ltd. This software is licensed under the
533 * GNU Affero General Public License version 3 (see the file LICENSE). */
534 YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
535
536@@ -44,6 +44,26 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
537 repository_name: '~someone/+git/somerepo',
538 information_type: 'Private'
539 }
540+ ],
541+ snaps: [
542+ {
543+ self_link: 'api/devel/~someone/+snap/somesnap',
544+ web_link: '/~someone/+snap/somesnap',
545+ id: '2',
546+ name: 'snap-name',
547+ information_type: 'Private'
548+ }
549+ ],
550+ ocirecipes: [
551+ {
552+ self_link: ('api/devel/~someone/proj/+oci/' +
553+ 'ociproj/+recipe/recipe'),
554+ web_link: ('~someone/proj/+oci/' +
555+ 'ociproj/+recipe/recipe'),
556+ name: 'ocirecipe-name',
557+ id: '2',
558+ information_type: 'Private'
559+ }
560 ]
561 }
562 };
563@@ -71,6 +91,8 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
564 bugs: window.LP.cache.bugs,
565 branches: window.LP.cache.branches,
566 gitrepositories: window.LP.cache.gitrepositories,
567+ snaps: window.LP.cache.snaps,
568+ ocirecipes: window.LP.cache.ocirecipes,
569 write_enabled: true
570 }, overrides);
571 window.LP.cache.grantee_data = config.grantees;
572@@ -326,10 +348,13 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
573 [window.LP.cache.bugs[0]],
574 [window.LP.cache.branches[0]],
575 [window.LP.cache.gitrepositories[0]],
576+ [], // specs
577+ [window.LP.cache.snaps[0]],
578+ [window.LP.cache.ocirecipes[0]],
579 true);
580 Y.Assert.areEqual(
581 'There are no shared bugs, Bazaar branches, Git ' +
582- 'repositories, or blueprints.',
583+ 'repositories, snap recipes, OCI recipes or blueprints.',
584 Y.one('#sharing-table-body tr').get('text'));
585 },
586
587diff --git a/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js b/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
588index 84f7930..bdcdcfb 100644
589--- a/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
590+++ b/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
591@@ -1,4 +1,4 @@
592-/* Copyright 2012-2015 Canonical Ltd. This software is licensed under the
593+/* Copyright 2012-2021 Canonical Ltd. This software is licensed under the
594 * GNU Affero General Public License version 3 (see the file LICENSE). */
595
596 YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
597@@ -49,6 +49,26 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
598 web_link: "/obsolete-junk/+spec/big-project"
599 }
600 ],
601+ snaps: [
602+ {
603+ self_link: 'api/devel/~someone/+snap/somesnap',
604+ web_link: '/~someone/+snap/somesnap',
605+ id: '2',
606+ name: 'snap-name',
607+ information_type: 'Private'
608+ }
609+ ],
610+ ocirecipes: [
611+ {
612+ self_link: ('api/devel/~someone/proj/+oci/' +
613+ 'ociproj/+recipe/recipe'),
614+ web_link: ('~someone/proj/+oci/' +
615+ 'ociproj/+recipe/recipe'),
616+ name: 'ocirecipe-name',
617+ id: '2',
618+ information_type: 'Private'
619+ }
620+ ],
621 grantee: {
622 displayname: 'Fred Bloggs',
623 self_link: '~fred'
624@@ -185,6 +205,51 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
625 Y.Assert.isTrue(confirmRemove_called);
626 },
627
628+ // Clicking a Snap remove link calls the
629+ // confirm_grant_removal method with the correct parameters.
630+ test_remove_snap_grant_click: function() {
631+ this.view = this._create_Widget();
632+ this.view.render();
633+ var confirmRemove_called = false;
634+ this.view.confirm_grant_removal = function(
635+ delete_link, artifact_uri, artifact_name, artifact_type) {
636+ Y.Assert.areEqual(
637+ 'api/devel/~someone/+snap/somesnap', artifact_uri);
638+ Y.Assert.areEqual('snap-name', artifact_name);
639+ Y.Assert.areEqual('snap', artifact_type);
640+ Y.Assert.areEqual(delete_link_to_click, delete_link);
641+ confirmRemove_called = true;
642+
643+ };
644+ var delete_link_to_click =
645+ Y.one('#sharing-table-body span[id=remove-snap-2] a');
646+ delete_link_to_click.simulate('click');
647+ Y.Assert.isTrue(confirmRemove_called);
648+ },
649+
650+ // Clicking an OCI recipe remove link calls the
651+ // confirm_grant_removal method with the correct parameters.
652+ test_remove_ocirecipe_grant_click: function() {
653+ this.view = this._create_Widget();
654+ this.view.render();
655+ var confirmRemove_called = false;
656+ this.view.confirm_grant_removal = function(
657+ delete_link, artifact_uri, artifact_name, artifact_type) {
658+ Y.Assert.areEqual(
659+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
660+ artifact_uri);
661+ Y.Assert.areEqual('ocirecipe-name', artifact_name);
662+ Y.Assert.areEqual('ocirecipe', artifact_type);
663+ Y.Assert.areEqual(delete_link_to_click, delete_link);
664+ confirmRemove_called = true;
665+
666+ };
667+ var delete_link_to_click =
668+ Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
669+ delete_link_to_click.simulate('click');
670+ Y.Assert.isTrue(confirmRemove_called);
671+ },
672+
673 //Test the behaviour of the removal confirmation dialog.
674 _test_confirm_grant_removal: function(click_ok) {
675 this.view = this._create_Widget();
676@@ -342,6 +407,88 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
677 Y.Assert.isTrue(remove_grant_success_called);
678 },
679
680+ // The perform_remove_grant method makes the expected XHR calls when a
681+ // Snap grant remove link is clicked.
682+ test_perform_remove_snap_grant: function() {
683+ var mockio = new Y.lp.testing.mockio.MockIo();
684+ var lp_client = new Y.lp.client.Launchpad({
685+ io_provider: mockio
686+ });
687+ this.view = this._create_Widget({
688+ lp_client: lp_client
689+ });
690+ this.view.render();
691+ var remove_grant_success_called = false;
692+ this.view.remove_grant_success = function(artifact_uri) {
693+ Y.Assert.areEqual(
694+ 'api/devel/~someone/+snap/somesnap', artifact_uri);
695+ remove_grant_success_called = true;
696+ };
697+ var delete_link =
698+ Y.one('#sharing-table-body span[id=remove-snap-2] a');
699+ this.view.perform_remove_grant(
700+ delete_link, 'api/devel/~someone/+snap/somesnap',
701+ 'snap');
702+ Y.Assert.areEqual(
703+ '/api/devel/+services/sharing',
704+ mockio.last_request.url);
705+ var expected_qs = '';
706+ expected_qs = Y.lp.client.append_qs(
707+ expected_qs, 'ws.op', 'revokeAccessGrants');
708+ expected_qs = Y.lp.client.append_qs(
709+ expected_qs, 'pillar', '/pillar');
710+ expected_qs = Y.lp.client.append_qs(
711+ expected_qs, 'grantee', '~fred');
712+ expected_qs = Y.lp.client.append_qs(
713+ expected_qs, 'snaps',
714+ 'api/devel/~someone/+snap/somesnap');
715+ Y.Assert.areEqual(expected_qs, mockio.last_request.config.data);
716+ mockio.last_request.successJSON({});
717+ Y.Assert.isTrue(remove_grant_success_called);
718+ },
719+
720+ // The perform_remove_grant method makes the expected XHR calls when an
721+ // OCI recipe grant remove link is clicked.
722+ test_perform_remove_ocirecipe_grant: function() {
723+ var mockio = new Y.lp.testing.mockio.MockIo();
724+ var lp_client = new Y.lp.client.Launchpad({
725+ io_provider: mockio
726+ });
727+ this.view = this._create_Widget({
728+ lp_client: lp_client
729+ });
730+ this.view.render();
731+ var remove_grant_success_called = false;
732+ this.view.remove_grant_success = function(artifact_uri) {
733+ Y.Assert.areEqual(
734+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
735+ artifact_uri);
736+ remove_grant_success_called = true;
737+ };
738+ var delete_link =
739+ Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
740+ this.view.perform_remove_grant(
741+ delete_link,
742+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
743+ 'ocirecipe');
744+ Y.Assert.areEqual(
745+ '/api/devel/+services/sharing',
746+ mockio.last_request.url);
747+ var expected_qs = '';
748+ expected_qs = Y.lp.client.append_qs(
749+ expected_qs, 'ws.op', 'revokeAccessGrants');
750+ expected_qs = Y.lp.client.append_qs(
751+ expected_qs, 'pillar', '/pillar');
752+ expected_qs = Y.lp.client.append_qs(
753+ expected_qs, 'grantee', '~fred');
754+ expected_qs = Y.lp.client.append_qs(
755+ expected_qs, 'ocirecipes',
756+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe');
757+ Y.Assert.areEqual(expected_qs, mockio.last_request.config.data);
758+ mockio.last_request.successJSON({});
759+ Y.Assert.isTrue(remove_grant_success_called);
760+ },
761+
762 // The remove bug grant callback updates the model and syncs the UI.
763 test_remove_bug_grant_success: function() {
764 this.view = this._create_Widget({anim_duration: 0});
765@@ -410,6 +557,40 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
766 'All specs are removed from the cache.');
767 },
768
769+ // The remove Snap grant callback updates the model and
770+ // syncs the UI.
771+ test_remove_snap_grant_success: function() {
772+ this.view = this._create_Widget({anim_duration: 0});
773+ this.view.render();
774+ var syncUI_called = false;
775+ this.view.syncUI = function() {
776+ syncUI_called = true;
777+ };
778+ this.view.remove_grant_success(
779+ 'api/devel/~someone/+snap/somesnap');
780+ Y.Assert.isTrue(syncUI_called);
781+ Y.Array.each(window.LP.cache.snaps, function(snap) {
782+ Y.Assert.areNotEqual(2, snap.id);
783+ });
784+ },
785+
786+ // The remove OCI recipe grant callback updates the model and
787+ // syncs the UI.
788+ test_remove_ocirecipe_grant_success: function() {
789+ this.view = this._create_Widget({anim_duration: 0});
790+ this.view.render();
791+ var syncUI_called = false;
792+ this.view.syncUI = function() {
793+ syncUI_called = true;
794+ };
795+ this.view.remove_grant_success(
796+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe');
797+ Y.Assert.isTrue(syncUI_called);
798+ Y.Array.each(window.LP.cache.ocirecipes, function(recipe) {
799+ Y.Assert.areNotEqual(2, recipe.id);
800+ });
801+ },
802+
803 // XHR calls display errors correctly.
804 _assert_error_displayed_on_failure: function(
805 artifact, invoke_operation) {
806@@ -491,6 +672,34 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
807 };
808 this._assert_error_displayed_on_failure('spec', invoke_remove);
809 },
810+
811+ // The perform_remove_grant method handles errors correctly with Snaps.
812+ test_perform_remove_snap_error: function() {
813+ var invoke_remove = function(view) {
814+ var delete_link =
815+ Y.one('#sharing-table-body span[id=remove-snap-2] a');
816+ view.perform_remove_grant(
817+ delete_link, 'api/devel/~someone/+snap/somesnap',
818+ 'snap');
819+ };
820+ this._assert_error_displayed_on_failure('snap', invoke_remove);
821+ },
822+
823+ // The perform_remove_grant method handles errors correctly with OCI
824+ // recipes.
825+ test_perform_remove_ocirecipe_error: function() {
826+ var invoke_remove = function(view) {
827+ var delete_link =
828+ Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
829+ view.perform_remove_grant(
830+ delete_link,
831+ 'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
832+ 'ocirecipe');
833+ };
834+ this._assert_error_displayed_on_failure(
835+ 'ocirecipe', invoke_remove);
836+ },
837+
838 // Test that syncUI works as expected.
839 test_syncUI: function() {
840 this.view = this._create_Widget();
841diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
842index eb4b135..2e59510 100644
843--- a/lib/lp/registry/services/sharingservice.py
844+++ b/lib/lp/registry/services/sharingservice.py
845@@ -856,7 +856,7 @@ class SharingService:
846
847 # Create a job to remove subscriptions for artifacts the grantee can no
848 # longer see.
849- return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
850+ getUtility(IRemoveArtifactSubscriptionsJobSource).create(
851 user, artifacts, grantee=grantee, pillar=pillar)
852
853 def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
854diff --git a/lib/lp/registry/templates/pillar-sharing-details.pt b/lib/lp/registry/templates/pillar-sharing-details.pt
855index dc907dd..d201bd3 100644
856--- a/lib/lp/registry/templates/pillar-sharing-details.pt
857+++ b/lib/lp/registry/templates/pillar-sharing-details.pt
858@@ -26,21 +26,31 @@
859 <div metal:fill-slot="main">
860
861 <div id="observer-summary">
862- <p>
863- <tal:bugs replace="view/shared_bugs_count">0</tal:bugs> bugs,
864- <tal:branches replace="view/shared_branches_count">0</tal:branches> Bazaar branches,
865- <tal:gitrepositories replace="view/shared_gitrepositories_count">0</tal:gitrepositories> Git repositories,
866- <tal:snaps replace="view/shared_snaps_count">0</tal:snaps> snaps,
867- and <tal:specifications
868- replace="view/shared_specifications_count">0</tal:specifications>
869- blueprints shared with <tal:name replace="view/person/displayname">
870- grantee</tal:name>.<br />
871-
872 <tal:is-team condition="view/person/is_team">
873- <tal:members>3</tal:members> team members can view these bugs,
874- Bazaar branches, Git repositories, and blueprints.
875+ <tal:members tal:replace="view/person/active_member_count" />
876+ team members can view these artifacts.
877 </tal:is-team>
878- </p>
879+ Shared with <tal:name replace="view/person/displayname">grantee</tal:name>:
880+ <ul class="bulleted">
881+ <li tal:condition="view/shared_bugs_count">
882+ <span tal:replace="view/shared_bugs_count" /> bugs
883+ </li>
884+ <li tal:condition="view/shared_branches_count">
885+ <span tal:replace="view/shared_branches_count" /> Bazaar branches
886+ </li>
887+ <li tal:condition="view/shared_gitrepositories_count">
888+ <span tal:replace="view/shared_gitrepositories_count" /> Git repositories
889+ </li>
890+ <li tal:condition="view/shared_ocirecipe_count">
891+ <span tal:replace="view/shared_ocirecipe_count" /> OCI recipes
892+ </li>
893+ <li tal:condition="view/shared_snaps_count">
894+ <span tal:replace="view/shared_snaps_count" /> snap recipes
895+ </li>
896+ <li tal:condition="view/shared_specifications_count">
897+ <span tal:replace="view/shared_specifications_count" /> blueprints
898+ </li>
899+ </ul>
900 </div>
901
902 <table id="shared-table" class="listing sortable">
903@@ -50,7 +60,8 @@
904 <thead>
905 <tr>
906 <th colspan="2" width="">
907- Subscribed Bug Report, Bazaar Branch, Git Repository, or Blueprint
908+ Subscribed bug report, Bazaar branch, Git repository, snap recipe,
909+ OCI recipe or blueprint
910 </th>
911 <th>
912 Information Type
913@@ -61,7 +72,7 @@
914 <tr>
915 <td colspan="3">
916 There are no shared bugs, Bazaar branches, Git repositories,
917- or blueprints.
918+ snap recipes, OCI recipes or blueprints.
919 </td>
920 </tr>
921 </tbody>