Merge lp:~wallyworld/launchpad/sharing-view-batching4-957663 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 14999
Proposed branch: lp:~wallyworld/launchpad/sharing-view-batching4-957663
Merge into: lp:launchpad
Diff against target: 548 lines (+165/-100)
9 files modified
lib/lp/registry/browser/pillar.py (+7/-8)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+13/-9)
lib/lp/registry/interfaces/accesspolicy.py (+3/-3)
lib/lp/registry/interfaces/sharingservice.py (+13/-2)
lib/lp/registry/model/accesspolicy.py (+45/-11)
lib/lp/registry/services/sharingservice.py (+20/-17)
lib/lp/registry/services/tests/test_sharingservice.py (+53/-42)
lib/lp/registry/templates/pillar-sharing.pt (+1/-1)
lib/lp/registry/tests/test_accesspolicy.py (+10/-7)
To merge this branch: bzr merge lp:~wallyworld/launchpad/sharing-view-batching4-957663
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Richard Harding (community) code* Approve
Review via email: mp+98820@code.launchpad.net

Commit message

Improve sharing service and access policy interfaces and rework queries to only load person objects once per batch.

Description of the change

== Implementation ==

Improve sharing service and access policy interfaces and rework queries to only load person objects once per batch.

Example of interface change:
The IAccessPolicyGrantFlatSource.findGranteePermissionsByPolicy() interface returned a list of tuples (IPerson, IAccessPolicy, string). The string was the permission name and although the name is required when hand data off to the view, this internal model method should have returned a tuple containing the SharingPermission enum object. Then it's up to the service methods to flatten the tuple out to the json data.

Also, the pillar sharing view had some methods changed to properties.

Before batching all the data for the view was loaded in a single query, joining Person, AccessPolicy and AccessPolicyGrantFlat. But this query returned multiple rows per person and it not suitable for batching. So the next iteration loaded all persons in the required batch and then made a second query to load the sharing permissions. The second query loaded person objects again. The time to do this was of the order of 10s of ms but William nagged me to death saying this was too expensive - loading persons in Launchpad is slow apparently. So I have changed the implementation of findGranteePermissionsByPolicy() to use a decorated result set so that the core batch query loads the persons and a pre_iteration hook bulk loads the permissions. So it's two queries (but with less joins) and only one loads persons. The second query is on the AccessPolicyGrantFlat table so is known to be fast.

SQL tracing shows that the batching infrastructure executes the core batching query twice due to a decorated result set being used. This is unfortunate. But I don't think the 2nd execution instantiates person objects.

== Tests ==

Check the existing tests run:
test_accesspolicy
test_sharingservice
test_pillar_sharing

The above ensures that external facing interfaces (eg those providing data to the view) work as before. Some of the tests needed tweaking due to the AccessPolicy model interface changes.

== 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/interfaces/accesspolicy.py
  lib/lp/registry/interfaces/sharingservice.py
  lib/lp/registry/model/accesspolicy.py
  lib/lp/registry/services/sharingservice.py
  lib/lp/registry/services/tests/test_sharingservice.py
  lib/lp/registry/templates/pillar-sharing.pt
  lib/lp/registry/tests/test_accesspolicy.py

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) :
review: Approve (code*)
Revision history for this message
j.c.sackett (jcsackett) wrote :

Hey Ian--

This mostly looks good; I just have one question below.

> === modified file 'lib/lp/registry/browser/pillar.py'
> --- lib/lp/registry/browser/pillar.py 2012-03-21 14:30:16 +0000
> +++ lib/lp/registry/browser/pillar.py 2012-03-22 13:04:20 +0000
> @@ -303,15 +303,17 @@
> size=config.launchpad.default_batch_size,
> range_factory=StormRangeFactory(sharees))
>
> - def shareeData(self):
> - """Return an `ITableBatchNavigator` for sharees."""
> + @property
> + def sharees(self):
> + """An `IBatchNavigator` for sharees."""
> if self._batch_navigator is None:
> - unbatchedSharees = self.unbatchedShareeData()
> + unbatchedSharees = self.unbatched_sharees
> self._batch_navigator = self._getBatchNavigator(unbatchedSharees)
> return self._batch_navigator
>
> - def unbatchedShareeData(self):
> - """Return all the sharees for a pillar."""
> + @property
> + def unbatched_sharees(self):
> + """All the sharees for a pillar."""
> return self._getSharingService().getPillarSharees(self.context)
>
> def initialize(self):

I'm not sure I understand the logic of making these properties; don't we
usually avoid that for things that might be expensive or slow, like
unbatched_sharees?

review: Needs Information
Revision history for this message
Ian Booth (wallyworld) wrote :

>
>> === modified file 'lib/lp/registry/browser/pillar.py'
>> --- lib/lp/registry/browser/pillar.py 2012-03-21 14:30:16 +0000
>> +++ lib/lp/registry/browser/pillar.py 2012-03-22 13:04:20 +0000
>> @@ -303,15 +303,17 @@
>> size=config.launchpad.default_batch_size,
>> range_factory=StormRangeFactory(sharees))
>>
>> - def shareeData(self):
>> - """Return an `ITableBatchNavigator` for sharees."""
>> + @property
>> + def sharees(self):
>> + """An `IBatchNavigator` for sharees."""
>> if self._batch_navigator is None:
>> - unbatchedSharees = self.unbatchedShareeData()
>> + unbatchedSharees = self.unbatched_sharees
>> self._batch_navigator = self._getBatchNavigator(unbatchedSharees)
>> return self._batch_navigator
>>
>> - def unbatchedShareeData(self):
>> - """Return all the sharees for a pillar."""
>> + @property
>> + def unbatched_sharees(self):
>> + """All the sharees for a pillar."""
>> return self._getSharingService().getPillarSharees(self.context)
>>
>> def initialize(self):
>
> I'm not sure I understand the logic of making these properties; don't we
> usually avoid that for things that might be expensive or slow, like
> unbatched_sharees?
>

Good question. The property only returns an unpopulated Storm result set
and so is not expensive to implement. The result set is lazily populated
when the first batch is retrieved. The code base seems inconsistent in
this area with regard to what's done. Since it's caused a question to be
asked, I'll change it back to avoid further confusion.

Revision history for this message
j.c.sackett (jcsackett) wrote :

> Since it's caused a question to be asked, I'll change it back to avoid further confusion.

Dig, per our discussion on mumble, go with your heart on this--there's no clear guideline either way for a view.

review: Approve

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-03-21 14:30:16 +0000
3+++ lib/lp/registry/browser/pillar.py 2012-03-22 22:33:25 +0000
4@@ -303,15 +303,15 @@
5 size=config.launchpad.default_batch_size,
6 range_factory=StormRangeFactory(sharees))
7
8- def shareeData(self):
9- """Return an `ITableBatchNavigator` for sharees."""
10+ def sharees(self):
11+ """An `IBatchNavigator` for sharees."""
12 if self._batch_navigator is None:
13- unbatchedSharees = self.unbatchedShareeData()
14+ unbatchedSharees = self.unbatched_sharees()
15 self._batch_navigator = self._getBatchNavigator(unbatchedSharees)
16 return self._batch_navigator
17
18- def unbatchedShareeData(self):
19- """Return all the sharees for a pillar."""
20+ def unbatched_sharees(self):
21+ """All the sharees for a pillar."""
22 return self._getSharingService().getPillarSharees(self.context)
23
24 def initialize(self):
25@@ -334,10 +334,9 @@
26 if len(view_names) != 1:
27 raise AssertionError("Ambiguous view name.")
28 cache.objects['view_name'] = view_names.pop()
29- batch_navigator = self.shareeData()
30+ batch_navigator = self.sharees()
31 cache.objects['sharee_data'] = (
32- self._getSharingService().getPillarShareeData(
33- self.context, batch_navigator.batch))
34+ self._getSharingService().jsonShareeData(batch_navigator.batch))
35
36 def _getBatchInfo(batch):
37 if batch is None:
38
39=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
40--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-21 14:30:16 +0000
41+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-22 22:33:25 +0000
42@@ -21,6 +21,7 @@
43
44 from lp.app.interfaces.services import IService
45 from lp.registry.enums import InformationType
46+from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantFlatSource
47 from lp.registry.model.pillar import PillarPerson
48 from lp.services.config import config
49 from lp.services.features.testing import FeatureFixture
50@@ -132,6 +133,7 @@
51 owner=self.owner, driver=self.driver)
52 login_person(self.driver)
53
54+
55 class PillarSharingViewTestMixin:
56 """Test the PillarSharingView."""
57
58@@ -139,7 +141,7 @@
59
60 def createSharees(self):
61 login_person(self.owner)
62- access_policy = self.factory.makeAccessPolicy(
63+ self.access_policy = self.factory.makeAccessPolicy(
64 pillar=self.pillar,
65 type=InformationType.PROPRIETARY)
66 self.grantees = []
67@@ -148,12 +150,12 @@
68 grantee = self.factory.makePerson(name=name)
69 self.grantees.append(grantee)
70 # Make access policy grant so that 'All' is returned.
71- self.factory.makeAccessPolicyGrant(access_policy, grantee)
72+ self.factory.makeAccessPolicyGrant(self.access_policy, grantee)
73 # Make access artifact grants so that 'Some' is returned.
74 artifact_grant = self.factory.makeAccessArtifactGrant()
75 self.factory.makeAccessPolicyArtifact(
76 artifact=artifact_grant.abstract_artifact,
77- policy=access_policy)
78+ policy=self.access_policy)
79 # Make grants for grantees in ascending order so we can slice off the
80 # first elements in the pillar observer results to check batching.
81 for x in range(10):
82@@ -207,12 +209,14 @@
83 cache = IJSONRequestCache(view.request)
84 self.assertIsNotNone(cache.objects.get('information_types'))
85 self.assertIsNotNone(cache.objects.get('sharing_permissions'))
86- aps = getUtility(IService, 'sharing')
87 batch_size = config.launchpad.default_batch_size
88- observers = aps.getPillarShareeData(
89- self.pillar, grantees=self.grantees[:batch_size])
90+ apgfs = getUtility(IAccessPolicyGrantFlatSource)
91+ sharees = apgfs.findGranteePermissionsByPolicy(
92+ [self.access_policy], self.grantees[:batch_size])
93+ sharing_service = getUtility(IService, 'sharing')
94+ sharee_data = sharing_service.jsonShareeData(sharees)
95 self.assertContentEqual(
96- observers, cache.objects.get('sharee_data'))
97+ sharee_data, cache.objects.get('sharee_data'))
98
99 def test_view_batch_data(self):
100 # Test the expected batching data is in the json request cache.
101@@ -220,7 +224,7 @@
102 view = create_initialized_view(self.pillar, name='+sharing')
103 cache = IJSONRequestCache(view.request)
104 # Test one expected data value (there are many).
105- next_batch = view.shareeData().batch.nextBatch()
106+ next_batch = view.sharees().batch.nextBatch()
107 self.assertContentEqual(
108 next_batch.range_memo, cache.objects.get('next')['memo'])
109
110@@ -228,7 +232,7 @@
111 # Test the view range factory is properly configured.
112 with FeatureFixture(ENABLED_FLAG):
113 view = create_initialized_view(self.pillar, name='+sharing')
114- range_factory = view.shareeData().batch.range_factory
115+ range_factory = view.sharees().batch.range_factory
116
117 def test_range_factory():
118 row = range_factory.resultset.get_plain_result_set()[0]
119
120=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
121--- lib/lp/registry/interfaces/accesspolicy.py 2012-03-21 10:39:00 +0000
122+++ lib/lp/registry/interfaces/accesspolicy.py 2012-03-22 22:33:25 +0000
123@@ -232,10 +232,10 @@
124 :param grantees: if not None, the result only includes people in the
125 specified list of grantees.
126 :return: a collection of (`IPerson`, `IAccessPolicy`, permission)
127- where permission is a SharingPermission value.
128- 'ALL' means the person has an access policy grant and can see all
129+ where permission is a SharingPermission enum value.
130+ ALL means the person has an access policy grant and can see all
131 artifacts for the associated pillar.
132- 'SOME' means the person only has specified access artifact grants.
133+ SOME means the person only has specified access artifact grants.
134 """
135
136 def findArtifactsByGrantee(grantee, policies):
137
138=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
139--- lib/lp/registry/interfaces/sharingservice.py 2012-03-21 20:39:00 +0000
140+++ lib/lp/registry/interfaces/sharingservice.py 2012-03-22 22:33:25 +0000
141@@ -60,8 +60,19 @@
142 @operation_parameters(
143 pillar=Reference(IPillar, title=_('Pillar'), required=True))
144 @operation_for_version('devel')
145- def getPillarShareeData(pillar, grantees=None):
146- """Return people/teams who can see pillar artifacts.
147+ def getPillarShareeData(pillar):
148+ """Return people/teams who can see pillar artifacts.
149+
150+ The result records are json data which includes:
151+ - person name
152+ - permissions they have for each information type.
153+ """
154+
155+ def jsonShareeData(grant_permissions):
156+ """Return people/teams who can see pillar artifacts.
157+
158+ :param grant_permissions: a list of (grantee, accesspolicy, permission)
159+ tuples.
160
161 The result records are json data which includes:
162 - person name
163
164=== modified file 'lib/lp/registry/model/accesspolicy.py'
165--- lib/lp/registry/model/accesspolicy.py 2012-03-21 10:39:00 +0000
166+++ lib/lp/registry/model/accesspolicy.py 2012-03-22 22:33:25 +0000
167@@ -25,7 +25,10 @@
168 from zope.component import getUtility
169 from zope.interface import implements
170
171-from lp.registry.enums import InformationType
172+from lp.registry.enums import (
173+ InformationType,
174+ SharingPermission,
175+ )
176 from lp.registry.interfaces.accesspolicy import (
177 IAccessArtifact,
178 IAccessArtifactGrant,
179@@ -37,6 +40,7 @@
180 )
181 from lp.registry.model.person import Person
182 from lp.services.database.bulk import create
183+from lp.services.database.decoratedresultset import DecoratedResultSet
184 from lp.services.database.enumcol import DBEnum
185 from lp.services.database.lpstorm import IStore
186 from lp.services.database.stormbase import StormBase
187@@ -350,13 +354,39 @@
188 def findGranteePermissionsByPolicy(cls, policies, grantees=None):
189 """See `IAccessPolicyGrantFlatSource`."""
190 ids = [policy.id for policy in policies]
191- sharing_permission_term = SQL("""
192- CASE(
193- MIN(COALESCE(artifact, 0)))
194- WHEN 0 THEN 'ALL'
195- ELSE 'SOME'
196- END
197- """)
198+
199+ # A cache for the sharing permissions, keyed on (grantee.id, policy.id)
200+ permissions_cache = {}
201+
202+ def set_permission(row):
203+ # row contains (grantee.id, policy.id, permission_placeholder)
204+ # Lookup the permission from the previously loaded cache.
205+ return (
206+ row[0], row[1], permissions_cache.get((row[0].id, row[1].id)))
207+
208+ def load_permissions(rows):
209+ # We now have the grantees and policies we want in the result so
210+ # load any corresponding permissions and cache them.
211+ person_ids = set(row[0].id for row in rows)
212+ policy_ids = set(row[1].id for row in rows)
213+ sharing_permission_term = SQL("""
214+ CASE(
215+ MIN(COALESCE(artifact, 0)))
216+ WHEN 0 THEN '%s'
217+ ELSE '%s'
218+ END
219+ """% (SharingPermission.ALL.name, SharingPermission.SOME.name))
220+ constraints = [
221+ cls.grantee_id.is_in(person_ids),
222+ cls.policy_id.is_in(policy_ids)]
223+ result_set = IStore(cls).find(
224+ (cls.grantee_id, cls.policy_id, sharing_permission_term),
225+ *constraints).group_by(cls.grantee_id, cls.policy_id)
226+ for (person_id, policy_id, permission) in result_set:
227+ permissions_cache[(person_id, policy_id)] = (
228+ SharingPermission.items[permission])
229+
230+ # The main result set has a placeholder for permission.
231 constraints = [
232 Person.id == cls.grantee_id,
233 AccessPolicy.id == cls.policy_id,
234@@ -364,9 +394,13 @@
235 if grantees:
236 grantee_ids = [grantee.id for grantee in grantees]
237 constraints.append(cls.grantee_id.is_in(grantee_ids))
238- return IStore(cls).find(
239- (Person, AccessPolicy, sharing_permission_term),
240- *constraints).group_by(Person, AccessPolicy)
241+ result_set = IStore(cls).find(
242+ (Person, AccessPolicy,
243+ SQL("'%s' as permission" % SharingPermission.NOTHING.name)),
244+ *constraints).config(distinct=True)
245+ return DecoratedResultSet(
246+ result_set,
247+ result_decorator=set_permission, pre_iter_hook=load_permissions)
248
249 @classmethod
250 def findArtifactsByGrantee(cls, grantee, policies):
251
252=== modified file 'lib/lp/registry/services/sharingservice.py'
253--- lib/lp/registry/services/sharingservice.py 2012-03-22 02:13:42 +0000
254+++ lib/lp/registry/services/sharingservice.py 2012-03-22 22:33:25 +0000
255@@ -102,21 +102,20 @@
256 # XXX 2012-03-22 wallyworld bug 961836
257 # We want to use person_sort_key(Person.displayname, Person.name) but
258 # StormRangeFactory doesn't support that yet.
259- grantees = ap_grant_flat.findGranteesByPolicy(
260+ grant_permissions = ap_grant_flat.findGranteePermissionsByPolicy(
261 policies).order_by(Person.displayname, Person.name)
262- return grantees
263+ return grant_permissions
264
265 @available_with_permission('launchpad.Driver', 'pillar')
266- def getPillarShareeData(self, pillar, grantees=None):
267+ def getPillarShareeData(self, pillar):
268 """See `ISharingService`."""
269- policies = getUtility(IAccessPolicySource).findByPillar([pillar])
270- ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
271- # XXX 2012-03-22 wallyworld bug 961836
272- # We want to use person_sort_key(Person.displayname, Person.name) but
273- # StormRangeFactory doesn't support that yet.
274- grant_permissions = ap_grant_flat.findGranteePermissionsByPolicy(
275- policies, grantees).order_by(Person.displayname, Person.name)
276+ grant_permissions = list(self.getPillarSharees(pillar))
277+ if not grant_permissions:
278+ return None
279+ return self.jsonShareeData(grant_permissions)
280
281+ def jsonShareeData(self, grant_permissions):
282+ """See `ISharingService`."""
283 result = []
284 person_by_id = {}
285 request = get_current_web_service_request()
286@@ -133,7 +132,8 @@
287 person_by_id[grantee.id] = person_data
288 result.append(person_data)
289 person_data = person_by_id[grantee.id]
290- person_data['permissions'][policy.type.name] = sharing_permission
291+ person_data['permissions'][policy.type.name] = (
292+ sharing_permission.name)
293 return result
294
295 @available_with_permission('launchpad.Edit', 'pillar')
296@@ -198,10 +198,13 @@
297 self.deletePillarSharee(pillar, sharee, info_types_for_nothing)
298
299 # Return sharee data to the caller.
300- sharees = self.getPillarShareeData(pillar, [sharee])
301- if not sharees:
302+ ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
303+ grant_permissions = list(ap_grant_flat.findGranteePermissionsByPolicy(
304+ all_pillar_policies, [sharee]))
305+ if not grant_permissions:
306 return None
307- return sharees[0]
308+ [sharee] = self.jsonShareeData(grant_permissions)
309+ return sharee
310
311 @available_with_permission('launchpad.Edit', 'pillar')
312 def deletePillarSharee(self, pillar, sharee,
313@@ -233,9 +236,9 @@
314
315 # Second delete any access artifact grants.
316 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
317- to_delete = ap_grant_flat.findArtifactsByGrantee(
318- sharee, pillar_policies)
319- if to_delete.count() > 0:
320+ to_delete = list(ap_grant_flat.findArtifactsByGrantee(
321+ sharee, pillar_policies))
322+ if len(to_delete) > 0:
323 accessartifact_grant_source = getUtility(
324 IAccessArtifactGrantSource)
325 accessartifact_grant_source.revokeByArtifact(to_delete)
326
327=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
328--- lib/lp/registry/services/tests/test_sharingservice.py 2012-03-21 20:39:00 +0000
329+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-03-22 22:33:25 +0000
330@@ -123,6 +123,19 @@
331 distro,
332 [InformationType.EMBARGOEDSECURITY, InformationType.USERDATA])
333
334+ def test_jsonShareeData(self):
335+ # jsonShareeData returns the expected data.
336+ access_policy = self.factory.makeAccessPolicy()
337+ grantee = self.factory.makePerson()
338+ sharees = self.service.jsonShareeData(
339+ [(grantee, access_policy, SharingPermission.ALL),
340+ (grantee, access_policy, SharingPermission.SOME)])
341+ expected_data = self._makeShareeData(
342+ grantee,
343+ [(access_policy.type, SharingPermission.ALL),
344+ (access_policy.type, SharingPermission.SOME)])
345+ self.assertContentEqual([expected_data], sharees)
346+
347 def _assert_getPillarShareeData(self, pillar):
348 # getPillarShareeData returns the expected data.
349 access_policy = self.factory.makeAccessPolicy(
350@@ -160,9 +173,15 @@
351 login_person(driver)
352 self._assert_getPillarShareeData(distro)
353
354- def test_getPillarShareeDataQueryCount(self):
355- # getPillarShareeData only should use 2 queries regardless of how many
356- # sharees are returned.
357+ def _assert_QueryCount(self, func):
358+ """ getPillarSharees[Data] only should use 3 queries.
359+
360+ 1. load access policies for pillar
361+ 2. load sharees
362+ 3. load permissions for sharee
363+
364+ Steps 2 and 3 are split out to allow batching on persons.
365+ """
366 driver = self.factory.makePerson()
367 product = self.factory.makeProduct(driver=driver)
368 login_person(driver)
369@@ -184,37 +203,22 @@
370 for x in range(5):
371 makeGrants()
372 with StormStatementRecorder() as recorder:
373- sharees = self.service.getPillarShareeData(product)
374+ sharees = list(func(product))
375 self.assertEqual(10, len(sharees))
376- self.assertThat(recorder, HasQueryCount(Equals(2)))
377+ self.assertThat(recorder, HasQueryCount(Equals(3)))
378 # Make some more grants and check again.
379 for x in range(5):
380 makeGrants()
381 with StormStatementRecorder() as recorder:
382- sharees = self.service.getPillarShareeData(product)
383+ sharees = list(func(product))
384 self.assertEqual(20, len(sharees))
385- self.assertThat(recorder, HasQueryCount(Equals(2)))
386-
387- def test_getPillarShareeData_filter_grantees(self):
388- # getPillarShareeData only returns grantees in the specified list.
389- driver = self.factory.makePerson()
390- pillar = self.factory.makeProduct(driver=driver)
391- login_person(driver)
392- access_policy = self.factory.makeAccessPolicy(
393- pillar=pillar,
394- type=InformationType.PROPRIETARY)
395- grantee_in_result = self.factory.makePerson()
396- grantee_not_in_result = self.factory.makePerson()
397- self.factory.makeAccessPolicyGrant(access_policy, grantee_in_result)
398- self.factory.makeAccessPolicyGrant(
399- access_policy, grantee_not_in_result)
400-
401- sharees = self.service.getPillarShareeData(pillar, [grantee_in_result])
402- expected_sharees = [
403- self._makeShareeData(
404- grantee_in_result,
405- [(InformationType.PROPRIETARY, SharingPermission.ALL)])]
406- self.assertContentEqual(expected_sharees, sharees)
407+ self.assertThat(recorder, HasQueryCount(Equals(3)))
408+
409+ def test_getPillarShareesQueryCount(self):
410+ self._assert_QueryCount(self.service.getPillarSharees)
411+
412+ def test_getPillarShareeDataQueryCount(self):
413+ self._assert_QueryCount(self.service.getPillarShareeData)
414
415 def _assert_getPillarShareeDataUnauthorized(self, pillar):
416 # getPillarShareeData raises an Unauthorized exception if the user is
417@@ -251,7 +255,9 @@
418 artifact=artifact_grant.abstract_artifact, policy=access_policy)
419
420 sharees = self.service.getPillarSharees(pillar)
421- expected_sharees = [grantee, artifact_grant.grantee]
422+ expected_sharees = [
423+ (grantee, access_policy, SharingPermission.ALL),
424+ (artifact_grant.grantee, access_policy, SharingPermission.SOME)]
425 self.assertContentEqual(expected_sharees, sharees)
426
427 def test_getProductSharees(self):
428@@ -347,9 +353,14 @@
429 expected_sharee_data = self._makeShareeData(
430 sharee, expected_permissions)
431 self.assertEqual(expected_sharee_data, sharee_data)
432- # Check that getPillarShareeData returns what we expect.
433- [sharee_data] = self.service.getPillarShareeData(pillar)
434- self.assertEqual(expected_sharee_data, sharee_data)
435+ # Check that getPillarSharees returns what we expect.
436+ expected_sharee_grants = [
437+ (sharee, policy, permission)
438+ for policy, permission in [
439+ (es_policy, SharingPermission.ALL),
440+ (ud_policy, SharingPermission.SOME)]]
441+ sharee_grants = list(self.service.getPillarSharees(pillar))
442+ self.assertContentEqual(expected_sharee_grants, sharee_grants)
443
444 def test_updateProjectGroupSharee_not_allowed(self):
445 # We cannot add sharees to ProjectGroups.
446@@ -455,18 +466,18 @@
447 if types_to_delete is not None:
448 expected_information_types = (
449 set(information_types).difference(types_to_delete))
450- remaining_grantee_person_data = self._makeShareeData(
451- grantee,
452- [(info_type, SharingPermission.ALL)
453- for info_type in expected_information_types])
454-
455- expected_data.append(remaining_grantee_person_data)
456- # Add the data for the other sharee.
457- another_person_data = self._makeShareeData(
458- another, [(information_types[0], SharingPermission.ALL)])
459+ expected_policies = [
460+ access_policy for access_policy in access_policies
461+ if access_policy.type in expected_information_types]
462+ expected_data = [
463+ (grantee, policy, SharingPermission.ALL)
464+ for policy in expected_policies]
465+ # Add the expected data for the other sharee.
466+ another_person_data = (
467+ another, access_policies[0], SharingPermission.ALL)
468 expected_data.append(another_person_data)
469 self.assertContentEqual(
470- expected_data, self.service.getPillarShareeData(pillar))
471+ expected_data, self.service.getPillarSharees(pillar))
472
473 def test_deleteProductShareeAll(self):
474 # Users with launchpad.Edit can delete all access for a sharee.
475
476=== modified file 'lib/lp/registry/templates/pillar-sharing.pt'
477--- lib/lp/registry/templates/pillar-sharing.pt 2012-03-19 14:03:51 +0000
478+++ lib/lp/registry/templates/pillar-sharing.pt 2012-03-22 22:33:25 +0000
479@@ -34,7 +34,7 @@
480 <li><a id="audit-link" class="sprite info" href='#'>Audit sharing</a></li>
481 </ul>
482
483- <div tal:define="batch_navigator view/shareeData">
484+ <div tal:define="batch_navigator view/sharees">
485 <tal:shareelisting content="structure batch_navigator/@@+sharee-table-view" />
486 </div>
487
488
489=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
490--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-21 10:39:00 +0000
491+++ lib/lp/registry/tests/test_accesspolicy.py 2012-03-22 22:33:25 +0000
492@@ -7,7 +7,10 @@
493 from testtools.matchers import AllMatch
494 from zope.component import getUtility
495
496-from lp.registry.enums import InformationType
497+from lp.registry.enums import (
498+ InformationType,
499+ SharingPermission,
500+ )
501 from lp.registry.interfaces.accesspolicy import (
502 IAccessArtifact,
503 IAccessArtifactGrant,
504@@ -467,7 +470,7 @@
505 apgfs.findGranteesByPolicy([
506 policy, another_policy, policy_with_no_grantees]))
507
508- def findGranteePermissionsByPolicy(self):
509+ def test_findGranteePermissionsByPolicy(self):
510 # findGranteePermissionsByPolicy() returns anyone with a grant for any
511 # of the policies or the policies' artifacts.
512 apgfs = getUtility(IAccessPolicyGrantFlatSource)
513@@ -477,14 +480,14 @@
514 policy = self.factory.makeAccessPolicy()
515 policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
516 self.assertContentEqual(
517- [(policy_grant.grantee, policy, 'ALL')],
518+ [(policy_grant.grantee, policy, SharingPermission.ALL)],
519 apgfs.findGranteePermissionsByPolicy(
520 [policy, policy_with_no_grantees]))
521
522 # But not people with grants on artifacts.
523 artifact_grant = self.factory.makeAccessArtifactGrant()
524 self.assertContentEqual(
525- [(policy_grant.grantee, policy, 'ALL')],
526+ [(policy_grant.grantee, policy, SharingPermission.ALL)],
527 apgfs.findGranteePermissionsByPolicy(
528 [policy, policy_with_no_grantees]))
529
530@@ -493,8 +496,8 @@
531 self.factory.makeAccessPolicyArtifact(
532 artifact=artifact_grant.abstract_artifact, policy=another_policy)
533 self.assertContentEqual(
534- [(policy_grant.grantee, policy, 'ALL'),
535- (artifact_grant.grantee, another_policy, 'SOME')],
536+ [(policy_grant.grantee, policy, SharingPermission.ALL),
537+ (artifact_grant.grantee, another_policy, SharingPermission.SOME)],
538 apgfs.findGranteePermissionsByPolicy([
539 policy, another_policy, policy_with_no_grantees]))
540
541@@ -513,7 +516,7 @@
542 self.factory.makeAccessPolicyGrant(
543 policy=policy, grantee=grantee_not_in_result)
544 self.assertContentEqual(
545- [(policy_grant.grantee, policy, 'ALL')],
546+ [(policy_grant.grantee, policy, SharingPermission.ALL)],
547 apgfs.findGranteePermissionsByPolicy(
548 [policy], [grantee_in_result]))
549