Merge lp:~jcsackett/launchpad/sharing-details into lp:launchpad

Proposed by j.c.sackett on 2012-03-19
Status: Merged
Approved by: j.c.sackett on 2012-03-19
Approved revision: no longer in the source branch.
Merged at revision: 14991
Proposed branch: lp:~jcsackett/launchpad/sharing-details
Merge into: lp:launchpad
Diff against target: 336 lines (+188/-8)
7 files modified
lib/lp/registry/browser/configure.zcml (+15/-2)
lib/lp/registry/browser/distribution.py (+6/-2)
lib/lp/registry/browser/pillar.py (+45/-2)
lib/lp/registry/browser/product.py (+3/-1)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+90/-0)
lib/lp/registry/templates/pillar-sharing-details.pt (+14/-0)
lib/lp/security.py (+15/-1)
To merge this branch: bzr merge lp:~jcsackett/launchpad/sharing-details
Reviewer Review Type Date Requested Status
Benji York (community) code 2012-03-19 Approve on 2012-03-19
Review via email: mp+98223@code.launchpad.net

Commit Message

Sets up infrastructure for the sharingdetails page for managing sharing (aka managing disclosure).

Description of the Change

Summary
=======
The managing sharing ui requires a page that shows the details of how a
particular person is being shared with by a pillar. This page anchors in the
infrastructure for that view. A subsequent branch
(lp:~jcsackett/launchpad/sharing-details-ui) adds the actual interaction
elements to the page.

Preimp
======
Spoke with Curtis Hovey about the necessary machinery and operation of
navigations and traversal for pillar person to allow the
+sharingdetails/{$person/name} url to function.

Implementation
==============
* A new view has been registered as the default view (+index) for pillar
  person.
* A navigation mixin has added to Distribution and Product Navigation, adding
  the traversal stepthrough for +sharingdetails. This mixin ensures that a
  result (a PillarPerson) is only provided if the person exists, and there are
  accesspolicyartifacts shared between the pillar and the person (i.e. there
  is actually sharing going on, and information to show).
* A security adapter for pillarperson has been added to check
  `launchpad.Driver`.
* A bunch of zcml has been added to register everything.

Tests
=====
bin/test -vvct test_pillar_sharing

QA
==
Using https://qastaging.launchpad.net/launchpad/+sharing, find or create a
user being shared with. Check the corresponding +sharingdetails link for that
person; an empty page with the right title and label should render.

Lint
====
Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/security.py
  lib/lp/registry/browser/product.py
  lib/lp/registry/templates/pillar-sharing-details.pt
  lib/lp/registry/browser/pillar.py
  lib/lp/registry/browser/tests/test_pillar_sharing.py

To post a comment you must log in.
Benji York (benji) wrote :

This branch looks good. The only suggestion I have is that it wouldn't
hurt to sort the __all__ list in lib/lp/registry/browser/pillar.py.

review: Approve (code)
j.c.sackett (jcsackett) wrote :

Thanks, I've sorted the __all__, per your point.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/configure.zcml'
2--- lib/lp/registry/browser/configure.zcml 2012-03-02 23:09:34 +0000
3+++ lib/lp/registry/browser/configure.zcml 2012-03-20 19:59:20 +0000
4@@ -1381,8 +1381,7 @@
5 name="+questions"/>
6 <browser:navigation
7 module="lp.registry.browser.product"
8- classes="
9- ProductNavigation"/>
10+ classes="ProductNavigation"/>
11 <browser:url
12 for="lp.registry.interfaces.product.IProduct"
13 path_expression="name"
14@@ -1428,6 +1427,20 @@
15 name="+sharing"
16 class="lp.registry.browser.pillar.PillarSharingView"
17 template="../templates/pillar-sharing.pt"/>
18+ <browser:url
19+ for="lp.registry.interfaces.pillar.IPillarPerson"
20+ path_expression="string:+sharingdetails/${person/name}"
21+ rootsite="mainsite"
22+ attribute_to_parent="pillar"/>
23+ <browser:page
24+ for="lp.registry.interfaces.pillar.IPillarPerson"
25+ permission="launchpad.Driver"
26+ name="+index"
27+ class="lp.registry.browser.pillar.PillarPersonSharingView"
28+ template="../templates/pillar-sharing-details.pt"/>
29+ <browser:defaultView
30+ for="lp.registry.interfaces.pillar.IPillarPerson"
31+ name="+index"/>
32 <browser:page
33 for="lp.registry.interfaces.product.IProduct"
34 permission="zope.Public"
35
36=== modified file 'lib/lp/registry/browser/distribution.py'
37--- lib/lp/registry/browser/distribution.py 2012-03-16 08:11:47 +0000
38+++ lib/lp/registry/browser/distribution.py 2012-03-20 19:59:20 +0000
39@@ -87,7 +87,10 @@
40 RegistryCollectionActionMenuBase,
41 )
42 from lp.registry.browser.objectreassignment import ObjectReassignmentView
43-from lp.registry.browser.pillar import PillarBugsMenu
44+from lp.registry.browser.pillar import (
45+ PillarBugsMenu,
46+ PillarNavigationMixin,
47+ )
48 from lp.registry.interfaces.distribution import (
49 IDerivativeDistribution,
50 IDistribution,
51@@ -135,7 +138,8 @@
52
53 class DistributionNavigation(
54 GetitemNavigation, BugTargetTraversalMixin, QuestionTargetTraversalMixin,
55- FAQTargetNavigationMixin, StructuralSubscriptionTargetTraversalMixin):
56+ FAQTargetNavigationMixin, StructuralSubscriptionTargetTraversalMixin,
57+ PillarNavigationMixin):
58
59 usedfor = IDistribution
60
61
62=== modified file 'lib/lp/registry/browser/pillar.py'
63--- lib/lp/registry/browser/pillar.py 2012-03-16 08:28:03 +0000
64+++ lib/lp/registry/browser/pillar.py 2012-03-20 19:59:20 +0000
65@@ -7,8 +7,10 @@
66
67 __all__ = [
68 'InvolvedMenu',
69+ 'PillarBugsMenu',
70 'PillarView',
71- 'PillarBugsMenu',
72+ 'PillarNavigationMixin',
73+ 'PillarPersonSharingView',
74 'PillarSharingView',
75 ]
76
77@@ -39,20 +41,27 @@
78 from lp.bugs.browser.structuralsubscription import (
79 StructuralSubscriptionMenuMixin,
80 )
81+from lp.registry.interfaces.accesspolicy import (
82+ IAccessPolicyGrantFlatSource,
83+ IAccessPolicySource,
84+ )
85 from lp.registry.interfaces.distributionsourcepackage import (
86 IDistributionSourcePackage,
87 )
88 from lp.registry.interfaces.distroseries import IDistroSeries
89 from lp.registry.interfaces.pillar import IPillar
90 from lp.registry.interfaces.projectgroup import IProjectGroup
91+from lp.registry.interfaces.person import IPersonSet
92+from lp.registry.model.pillar import PillarPerson
93 from lp.services.propertycache import cachedproperty
94 from lp.services.features import getFeatureFlag
95 from lp.services.webapp.authorization import check_permission
96-from lp.services.webapp.menu import (
97+from lp.services.webapp import (
98 ApplicationMenu,
99 enabled_with_permission,
100 Link,
101 NavigationMenu,
102+ stepthrough,
103 )
104 from lp.services.webapp.publisher import (
105 LaunchpadView,
106@@ -60,6 +69,22 @@
107 )
108
109
110+class PillarNavigationMixin:
111+
112+ @stepthrough('+sharingdetails')
113+ def traverse_details(self, name):
114+ """Traverse to the sharing details for a given person."""
115+ person = getUtility(IPersonSet).getByName(name)
116+ if person is None:
117+ return None
118+ policies = getUtility(IAccessPolicySource).findByPillar([self.context])
119+ source = getUtility(IAccessPolicyGrantFlatSource)
120+ artifacts = source.findArtifactsByGrantee(person, policies)
121+ if artifacts.is_empty():
122+ return None
123+ return PillarPerson.create(self.context, person)
124+
125+
126 class IInvolved(Interface):
127 """A marker interface for getting involved."""
128
129@@ -281,3 +306,21 @@
130 cache.objects['information_types'] = self.information_types
131 cache.objects['sharing_permissions'] = self.sharing_permissions
132 cache.objects['sharee_data'] = self.sharee_data
133+
134+
135+class PillarPersonSharingView(LaunchpadView):
136+
137+ page_title = "Person or team"
138+ label = "Information shared with person or team"
139+
140+ def initialize(self):
141+ enabled_flag = 'disclosure.enhanced_sharing.enabled'
142+ enabled = bool(getFeatureFlag(enabled_flag))
143+ if not enabled:
144+ raise Unauthorized("This feature is not yet available.")
145+
146+ self.pillar = self.context.pillar
147+ self.person = self.context.person
148+
149+ self.label = "Information shared with %s" % self.person.displayname
150+ self.page_title = "%s" % self.person.displayname
151
152=== modified file 'lib/lp/registry/browser/product.py'
153--- lib/lp/registry/browser/product.py 2012-03-16 08:11:47 +0000
154+++ lib/lp/registry/browser/product.py 2012-03-20 19:59:20 +0000
155@@ -149,6 +149,7 @@
156 )
157 from lp.registry.browser.pillar import (
158 PillarBugsMenu,
159+ PillarNavigationMixin,
160 PillarView,
161 )
162 from lp.registry.browser.productseries import get_series_branch_error
163@@ -211,7 +212,8 @@
164 class ProductNavigation(
165 Navigation, BugTargetTraversalMixin,
166 FAQTargetNavigationMixin, HasCustomLanguageCodesTraversalMixin,
167- QuestionTargetTraversalMixin, StructuralSubscriptionTargetTraversalMixin):
168+ QuestionTargetTraversalMixin, StructuralSubscriptionTargetTraversalMixin,
169+ PillarNavigationMixin):
170
171 usedfor = IProduct
172
173
174=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
175--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-16 07:07:19 +0000
176+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-20 19:59:20 +0000
177@@ -10,9 +10,11 @@
178 from BeautifulSoup import BeautifulSoup
179 from lazr.restful.interfaces import IJSONRequestCache
180 from zope.component import getUtility
181+from zope.publisher.interfaces import NotFound
182 from zope.security.interfaces import Unauthorized
183
184 from lp.app.interfaces.services import IService
185+from lp.registry.model.pillar import PillarPerson
186 from lp.services.features.testing import FeatureFixture
187 from lp.services.webapp.publisher import canonical_url
188 from lp.testing import (
189@@ -31,6 +33,94 @@
190 WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}
191
192
193+class PillarSharingDetailsMixin:
194+ """Test the pillar sharing details view."""
195+
196+ layer = DatabaseFunctionalLayer
197+
198+ def getPillarPerson(self, person=None, with_sharing=True):
199+ if person is None:
200+ person = self.factory.makePerson()
201+ if with_sharing:
202+ if self.pillar_type == 'product':
203+ bug = self.factory.makeBug(product=self.pillar, private=True)
204+ elif self.pillar_type == 'distribution':
205+ bug = self.factory.makeBug(
206+ distribution=self.pillar, private=True)
207+ artifact = self.factory.makeAccessArtifact(concrete=bug)
208+ policy = self.factory.makeAccessPolicy(pillar=self.pillar)
209+ self.factory.makeAccessPolicyArtifact(
210+ artifact=artifact, policy=policy)
211+ self.factory.makeAccessArtifactGrant(
212+ artifact=artifact, grantee=person, grantor=self.pillar.owner)
213+
214+ return PillarPerson(self.pillar, person)
215+
216+ def test_view_traverses_plus_sharingdetails(self):
217+ # The traversed url in the app is pillar/+sharingdetails/person
218+ with FeatureFixture(ENABLED_FLAG):
219+ # We have to do some fun url hacking to force the traversal a user
220+ # encounters.
221+ pillarperson = self.getPillarPerson()
222+ expected = pillarperson.person.displayname
223+ url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
224+ pillarperson.pillar.name, pillarperson.person.name)
225+ browser = self.getUserBrowser(user=self.driver, url=url)
226+ self.assertEqual(expected, browser.title)
227+
228+ def test_not_found_without_sharing(self):
229+ # If there is no sharing between pillar and person, NotFound is the
230+ # result.
231+ with FeatureFixture(ENABLED_FLAG):
232+ # We have to do some fun url hacking to force the traversal a user
233+ # encounters.
234+ pillarperson = self.getPillarPerson(with_sharing=False)
235+ url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
236+ pillarperson.pillar.name, pillarperson.person.name)
237+ browser = self.getUserBrowser(user=self.driver)
238+ self.assertRaises(NotFound, browser.open, url)
239+
240+ def test_init_without_feature_flag(self):
241+ # We need a feature flag to enable the view.
242+ pillarperson = self.getPillarPerson()
243+ self.assertRaises(
244+ Unauthorized, create_initialized_view, pillarperson, '+index')
245+
246+ def test_init_with_feature_flag(self):
247+ # The view works with a feature flag.
248+ with FeatureFixture(ENABLED_FLAG):
249+ pillarperson = self.getPillarPerson()
250+ view = create_initialized_view(pillarperson, '+index')
251+ self.assertEqual(pillarperson.person.displayname, view.page_title)
252+
253+
254+class TestProductSharingDetailsView(
255+ TestCaseWithFactory, PillarSharingDetailsMixin):
256+
257+ pillar_type = 'product'
258+
259+ def setUp(self):
260+ super(TestProductSharingDetailsView, self).setUp()
261+ self.driver = self.factory.makePerson()
262+ self.owner = self.factory.makePerson()
263+ self.pillar = self.factory.makeProduct(
264+ owner=self.owner, driver=self.driver)
265+ login_person(self.driver)
266+
267+
268+class TestDistributionSharingDetailsView(
269+ TestCaseWithFactory, PillarSharingDetailsMixin):
270+
271+ pillar_type = 'distribution'
272+
273+ def setUp(self):
274+ super(TestDistributionSharingDetailsView, self).setUp()
275+ self.driver = self.factory.makePerson()
276+ self.owner = self.factory.makePerson()
277+ self.pillar = self.factory.makeProduct(
278+ owner=self.owner, driver=self.driver)
279+ login_person(self.driver)
280+
281 class PillarSharingViewTestMixin:
282 """Test the PillarSharingView."""
283
284
285=== added file 'lib/lp/registry/templates/pillar-sharing-details.pt'
286--- lib/lp/registry/templates/pillar-sharing-details.pt 1970-01-01 00:00:00 +0000
287+++ lib/lp/registry/templates/pillar-sharing-details.pt 2012-03-20 19:59:20 +0000
288@@ -0,0 +1,14 @@
289+<html
290+ xmlns="http://www.w3.org/1999/xhtml"
291+ xmlns:tal="http://xml.zope.org/namespaces/tal"
292+ xmlns:metal="http://xml.zope.org/namespaces/metal"
293+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
294+ metal:use-macro="view/macro:page/main_only"
295+ i18n:domain="launchpad"
296+>
297+
298+<body>
299+ <div metal:fill-slot="main">
300+ </div>
301+</body>
302+</html>
303
304=== modified file 'lib/lp/security.py'
305--- lib/lp/security.py 2012-02-15 00:57:40 +0000
306+++ lib/lp/security.py 2012-03-20 19:59:20 +0000
307@@ -124,7 +124,10 @@
308 ITeam,
309 PersonVisibility,
310 )
311-from lp.registry.interfaces.pillar import IPillar
312+from lp.registry.interfaces.pillar import (
313+ IPillar,
314+ IPillarPerson,
315+ )
316 from lp.registry.interfaces.poll import (
317 IPoll,
318 IPollOption,
319@@ -337,6 +340,17 @@
320 user.in_registry_experts)
321
322
323+class PillarPersonSharingDriver(AuthorizationBase):
324+ usedfor = IPillarPerson
325+ permission = 'launchpad.Driver'
326+
327+ def checkAuthenticated(self, user):
328+ """The Admins & Commercial Admins can see inactive pillars."""
329+ return (user.in_admin or
330+ user.isOwner(self.obj.pillar) or
331+ user.isOneOfDrivers(self.obj.pillar))
332+
333+
334 class EditAccountBySelfOrAdmin(AuthorizationBase):
335 permission = 'launchpad.Edit'
336 usedfor = IAccount