Merge lp:~adeuring/launchpad/authentication-for-private-products-2 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16148
Proposed branch: lp:~adeuring/launchpad/authentication-for-private-products-2
Merge into: lp:launchpad
Diff against target: 1795 lines (+733/-105)
38 files modified
lib/lp/answers/tests/test_question_webservice.py (+5/-3)
lib/lp/app/browser/launchpad.py (+25/-0)
lib/lp/app/browser/tests/test_launchpad.py (+104/-1)
lib/lp/app/model/launchpad.py (+1/-1)
lib/lp/blueprints/browser/tests/test_specification.py (+11/-4)
lib/lp/blueprints/browser/tests/test_views.py (+1/-1)
lib/lp/blueprints/tests/test_webservice.py (+6/-4)
lib/lp/bugs/browser/tests/test_bugtask.py (+2/-2)
lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt (+2/-1)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt (+2/-1)
lib/lp/bugs/stories/patches-view/patches-view.txt (+2/-0)
lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt (+2/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+5/-3)
lib/lp/bugs/tests/test_bugs_webservice.py (+7/-4)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+3/-2)
lib/lp/code/browser/tests/test_branch.py (+1/-1)
lib/lp/code/browser/tests/test_product.py (+4/-2)
lib/lp/code/stories/branches/xx-product-branches.txt (+5/-2)
lib/lp/code/stories/webservice/xx-code-import.txt (+3/-2)
lib/lp/registry/browser/tests/test_milestone.py (+4/-2)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+3/-3)
lib/lp/registry/browser/tests/test_product.py (+1/-1)
lib/lp/registry/browser/tests/test_productseries_views.py (+9/-2)
lib/lp/registry/browser/tests/test_sourcepackage_views.py (+14/-6)
lib/lp/registry/configure.zcml (+25/-11)
lib/lp/registry/interfaces/product.py (+22/-16)
lib/lp/registry/model/product.py (+35/-0)
lib/lp/registry/services/tests/test_sharingservice.py (+4/-2)
lib/lp/registry/tests/test_pillaraffiliation.py (+1/-1)
lib/lp/registry/tests/test_product.py (+355/-2)
lib/lp/registry/tests/test_product_webservice.py (+24/-15)
lib/lp/registry/tests/test_subscribers.py (+4/-1)
lib/lp/security.py (+25/-0)
lib/lp/testing/factory.py (+3/-3)
lib/lp/testing/pages.py (+3/-1)
lib/lp/translations/browser/tests/test_noindex.py (+6/-1)
lib/lp/translations/stories/importqueue/xx-entry-details.txt (+2/-1)
lib/lp/translations/stories/webservice/xx-potemplate.txt (+2/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/authentication-for-private-products-2
Reviewer Review Type Date Requested Status
William Grant code Approve
Curtis Hovey (community) code Approve
Review via email: mp+129459@code.launchpad.net

Commit message

policy grant based access checks for private products.

Description of the change

Another attempt to activate permission checks for private products.

pre-imp calls with suínzui and flacoste

The first attempt, merged in r16090, reviewed here
https://code.launchpad.net/~adeuring/launchpad/correct-permission-check-for-iproduct/+merge/127518
and here
https://code.launchpad.net/~adeuring/launchpad/product-sharing-sec-adapter/+merge/127473
had to be reverted becaused is caused lots of "permission denied"
errors.

The causes of these error:

(1) Nearly all properties of IProduct required the permission
launchpad.View . Before, only IPillar was guarded by this
permission, most other attributes were public.

(2) The security adapter, class ViewProduct, inherited from
the checker for IPillar, class ViewPillar. And ViewProduct
further limited the access so that only people with policy
grants for private products get acces to those product. But
it also denided access to most properties of inactive public
products, by also calling ViewPillar.check[Un]Authenticated()

(3) Lots of pages retrieve "related products", for example
person pages, or bug pages. It often happens that attributes
like product.name, product.displayname or subscription related
stuff, were accessed -- even for inactive products. Data for
inactive products is later dropped, but obviously at a very
late stage during rendering. It seems that inactive products
are quite often filtered out at a very late stage of page
rendering.

This branch fixes these problems by again allowing access to
all attributes that require the permisison launchpad.View
for inactive public products, by not checking the flag
product.active flag in ProductView.check[Un]authenticated()

This has again another unwanted side effect:

All users can see inactive public products, while we want to
present a 404 page in this case. The reason:

lp.app.browser.launchpad.Launchpad.traverse() checks if
the user has the permission launchpad.View for a given pillar.

This check worked before because launchpad.View was only
required for IPillar (see class ViewPillar), so all these
eventually unnecessary accesses to product properties were
possible even if a user did not have the permission lp.View
-- but with the new permission checker ViewProduct in place,
users would see inactive products like admins, just with a
warning "this project ic inactive".

So we simply can't call check_permission('lp.View', product)
in Launchpad.traverse() any longer but need to explicitly check
for inactive products.

Note that this "special rule" for products will be needed anyway
once we allow artifact grants for private products: This will
require another permission, launchpad.LimitedView, so that users
with grants for artifacts can traverse to branches or bugs.
Aand the traverse() method should then check if users

tests:

./bin/test -vvt lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
./bin/test -vvt lp.app.browser.tests.test_launchpad.TestProductTraversal
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_proprietary_product
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_public_inactive_product

no lint.

The complete diff against trunk is quite large.

r16117 is just a "rollback of the rollback" from r16112.

The relevant changes are in r16118. The diff r16117..16118:

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2012-07-09 13:18:34 +0000
+++ lib/lp/app/browser/launchpad.py 2012-10-12 13:02:05 +0000
@@ -105,9 +105,11 @@
 from lp.registry.interfaces.pillar import IPillarNameSet
 from lp.registry.interfaces.product import (
     InvalidProductName,
+ IProduct,
     IProductSet,
     )
 from lp.registry.interfaces.projectgroup import IProjectGroupSet
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
 from lp.services.config import config
 from lp.services.helpers import intOrZero
@@ -763,6 +765,29 @@

         pillar = getUtility(IPillarNameSet).getByName(
             name, ignore_inactive=False)
+
+ if (pillar is not None and IProduct.providedBy(pillar) and
+ not pillar.active):
+ # Emergency brake for public but inactive products:
+ # These products should not be shown to ordinary users.
+ # The root problem is that many views iterate over products,
+ # inactive products included, and access attributes like
+ # name, displayname or call canonical_url(product) --
+ # and finally throw the data away, if the product is
+ # inactive. So we cannot make these attributes inaccessible
+ # for inactive public products. On the other hand, we
+ # require the permission launchpad.View to protect private
+ # products.
+ # This means that we cannot simply check if the current
+ # user has the permission launchpad.View for an inactive
+ # product.
+ user = getUtility(ILaunchBag).user
+ if user is None:
+ return None
+ user = IPersonRoles(user)
+ if (not user.in_commercial_admin and not user.in_admin and
+ not user.in_registry_experts):
+ return None
         if pillar is not None and check_permission('launchpad.View', pillar):
             if pillar.name != name:
                 # This pillar was accessed through one of its aliases, so we

=== modified file 'lib/lp/app/browser/tests/test_launchpad.py'
--- lib/lp/app/browser/tests/test_launchpad.py 2012-09-17 15:19:10 +0000
+++ lib/lp/app/browser/tests/test_launchpad.py 2012-10-12 13:02:05 +0000
@@ -20,8 +20,12 @@
 from lp.app.enums import InformationType
 from lp.app.errors import GoneError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.interfaces.services import IService
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
-from lp.registry.enums import PersonVisibility
+from lp.registry.enums import (
+ PersonVisibility,
+ SharingPermission,
+ )
 from lp.registry.interfaces.person import IPersonSet
 from lp.services.identity.interfaces.account import AccountStatus
 from lp.services.webapp import canonical_url
@@ -468,3 +472,108 @@
             reg.name for reg in iter_view_registrations(macros.__class__))
         self.assertIn('+base-layout-macros', names)
         self.assertNotIn('+related-pages', names)
+
+
+class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestProductTraversal, self).setUp()
+ self.active_public_product = self.factory.makeProduct()
+ self.inactive_public_product = self.factory.makeProduct()
+ removeSecurityProxy(self.inactive_public_product).active = False
+ self.proprietary_product_owner = self.factory.makePerson()
+ self.active_proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ self.inactive_proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ removeSecurityProxy(self.inactive_proprietary_product).active = False
+
+ def traverse_to_active_public_product(self):
+ segment = self.active_public_product.name
+ self.traverse(segment, segment)
+
+ def traverse_to_inactive_public_product(self):
+ segment = removeSecurityProxy(self.inactive_public_product).name
+ self.traverse(segment, segment)
+
+ def traverse_to_active_proprietary_product(self):
+ segment = removeSecurityProxy(self.active_proprietary_product).name
+ self.traverse(segment, segment)
+
+ def traverse_to_inactive_proprietary_product(self):
+ segment = removeSecurityProxy(self.inactive_proprietary_product).name
+ self.traverse(segment, segment)
+
+ def test_access_for_anon(self):
+ # Anonymous users can see only public active products.
+ with person_logged_in(ANONYMOUS):
+ self.traverse_to_active_public_product()
+ # Access to other products raises a NotFound error.
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_active_proprietary_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def test_access_for_ordinary_users(self):
+ # Ordinary logged in users can see only public active products.
+ with person_logged_in(self.factory.makePerson()):
+ self.traverse_to_active_public_product()
+ # Access to other products raises a NotFound error.
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_active_proprietary_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def test_access_for_person_with_pillar_grant(self):
+ # Persons with a policy grant for a proprietary product can
+ # access this product, if it is active.
+ user = self.factory.makePerson()
+ with person_logged_in(self.proprietary_product_owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.active_proprietary_product, user,
+ self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.inactive_proprietary_product, user,
+ self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ self.traverse_to_active_public_product()
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.traverse_to_active_proprietary_product()
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def check_admin_access(self, user):
+ with person_logged_in(user):
+ self.traverse_to_active_public_product()
+ self.traverse_to_inactive_public_product()
+ self.traverse_to_active_proprietary_product()
+ self.traverse_to_inactive_proprietary_product()
+
+ def test_access_for_persons_with_special_permissions(self):
+ # Admins have access all products, including inactive and propretary
+ # products.
+ self.check_admin_access(getUtility(IPersonSet).getByName('name16'))
+ # Registry experts can access to all products.
+ registry_expert = self.factory.makePerson()
+ registry = getUtility(ILaunchpadCelebrities).registry_experts
+ with person_logged_in(registry.teamowner):
+ registry.addMember(registry_expert, registry.teamowner)
+ self.check_admin_access(registry_expert)
+ # Commercial admins have access to all products.
+ commercial_admin = self.factory.makePerson()
+ commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
+ with person_logged_in(commercial_admins.teamowner):
+ commercial_admins.addMember(
+ commercial_admin, commercial_admins.teamowner)
+ self.check_admin_access(registry_expert)

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/model/product.py 2012-10-12 13:02:05 +0000
@@ -1548,13 +1548,21 @@
             return False
         if user.id in self._known_viewers:
             return True
- # We want an actual Storm Person.
+ # We need the plain Storm Person object for the SQL query below
+ # but an IPersonRoles object for the team membership checks.
         if IPersonRoles.providedBy(user):
- user = user.person
+ plain_user = user.person
+ else:
+ plain_user = user
+ user = IPersonRoles(user)
+ if (user.in_commercial_admin or user.in_admin or
+ user.in_registry_experts):
+ self._known_viewers.add(user.id)
+ return True
         policy = getUtility(IAccessPolicySource).find(
             [(self, self.information_type)]).one()
         grants_for_user = getUtility(IAccessPolicyGrantSource).find(
- [(policy, user)])
+ [(policy, plain_user)])
         if grants_for_user.is_empty():
             return False
         self._known_viewers.add(user.id)

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-12 13:02:05 +0000
@@ -59,11 +59,13 @@
     IAccessPolicySource,
     )
 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
+from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import (
     IProduct,
     IProductSet,
     License,
     )
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.product import (
     Product,
@@ -741,6 +743,24 @@
             for attribute_name in names:
                 getattr(product, attribute_name)

+ def test_access_launchpad_View_public_inactive_product(self):
+ # Everybody, including anonymous users, has access to
+ # properties of public but inactvie products that require
+ # the permission launchpad.View.
+ product = self.factory.makeProduct()
+ removeSecurityProxy(product).active = False
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
     def test_access_launchpad_View_proprietary_product(self):
         # Only people with grants for a private product can access
         # attributes protected by the permission launchpad.View.
@@ -770,6 +790,26 @@
         with person_logged_in(ordinary_user):
             for attribute_name in names:
                 getattr(product, attribute_name)
+ # Admins can access proprietary products.
+ with person_logged_in(getUtility(IPersonSet).getByName('name16')):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ registry_expert = self.factory.makePerson()
+ registry = getUtility(ILaunchpadCelebrities).registry_experts
+ with person_logged_in(registry.teamowner):
+ registry.addMember(registry_expert, registry.teamowner)
+ with person_logged_in(registry_expert):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Commercial admins have access to all products.
+ commercial_admin = self.factory.makePerson()
+ commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
+ with person_logged_in(commercial_admins.teamowner):
+ commercial_admins.addMember(
+ commercial_admin, commercial_admins.teamowner)
+ with person_logged_in(commercial_admin):
+ for attribute_name in names:
+ getattr(product, attribute_name)

     def test_access_launchpad_AnyAllowedPerson_public_product(self):
         # Only logged in persons have access to properties of public products
@@ -882,6 +922,16 @@
                 self.assertEqual(
                 queries_for_first_user_access, len(recorder.queries))

+ def test_userCanView_works_with_IPersonRoles(self):
+ # userCanView() maintains a cache of users known to have the
+ # permission to access a product.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ user = self.factory.makePerson()
+ product.userCanView(user)
+ product.userCanView(IPersonRoles(user))
+

 class TestProductBugInformationTypes(TestCaseWithFactory):

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-10-09 10:28:02 +0000
+++ lib/lp/security.py 2012-10-12 13:02:05 +0000
@@ -14,13 +14,8 @@
 from operator import methodcaller

 from storm.expr import (
- And,
- Exists,
- Or,
     Select,
- SQL,
     Union,
- With,
     )
 from zope.component import (
     getUtility,
@@ -177,7 +172,6 @@
     )
 from lp.registry.interfaces.wikiname import IWikiName
 from lp.registry.model.person import Person
-from lp.registry.model.teammembership import TeamParticipation
 from lp.services.config import config
 from lp.services.database.lpstorm import IStore
 from lp.services.identity.interfaces.account import IAccount
@@ -431,19 +425,15 @@
         return user.isOwner(self.obj) or user.in_admin

-class ViewProduct(ViewPillar):
+class ViewProduct(AuthorizationBase):
     permission = 'launchpad.View'
     usedfor = IProduct

     def checkAuthenticated(self, user):
- return (
- super(ViewProduct, self).checkAuthenticated(user) and
- self.obj.userCanView(user))
+ return self.obj.userCanView(user)

     def checkUnauthenticated(self):
- return (
- self.obj.information_type in PUBLIC_INFORMATION_TYPES and
- super(ViewProduct, self).checkUnauthenticated())
+ return self.obj.information_type in PUBLIC_INFORMATION_TYPES

 class ChangeProduct(ViewProduct):

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

This looks good. Thank you. I think one test can be simplified (and it looks like it is testing the wrong user at the end). this test duplicates code performed by 'celebrity_logged_in'. We also want to avoid sampledata since Foo Bar is more than just an Admin.

203 + def test_access_for_persons_with_special_permissions(self):
204 + # Admins have access all products, including inactive and propretary
205 + # products.
206 + self.check_admin_access(getUtility(IPersonSet).getByName('name16'))
207 + # Registry experts can access to all products.
208 + registry_expert = self.factory.makePerson()
209 + registry = getUtility(ILaunchpadCelebrities).registry_experts
210 + with person_logged_in(registry.teamowner):
211 + registry.addMember(registry_expert, registry.teamowner)
212 + self.check_admin_access(registry_expert)
213 + # Commercial admins have access to all products.
214 + commercial_admin = self.factory.makePerson()
215 + commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
216 + with person_logged_in(commercial_admins.teamowner):
217 + commercial_admins.addMember(
218 + commercial_admin, commercial_admins.teamowner)
219 + self.check_admin_access(registry_expert)

Could be
    def test_access_for_persons_with_special_permissions(self):
        # Admins have access all products, including inactive and propretary
        # products.
        with celebrity_logged_in('admin') as admin:
            self.check_admin_access(admin)
        with celebrity_logged_in('registry_experts') as registry_expert:
            self.check_admin_access(registry_expert)
        with celebrity_logged_in('commercial_admins') as commercial_admin:
            self.check_admin_access(commercial_admin)

review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

I'd still like to see an emergency feature flag in userCanView, in case we end up with the initial private projects on production breaking something unexpected. But otherwise this looks pretty good, particularly the improvements around handling deactivated products.

969 + if (user.in_commercial_admin or user.in_admin or
970 + user.in_registry_experts):

There are registry admins from outside Canonical, so ~registry must not be able to see all private projects.

Also, you can also easily fix bug #1061933 while you're here.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/answers/tests/test_question_webservice.py'
--- lib/lp/answers/tests/test_question_webservice.py 2012-10-08 10:07:11 +0000
+++ lib/lp/answers/tests/test_question_webservice.py 2012-10-15 15:39:22 +0000
@@ -10,6 +10,7 @@
10from simplejson import dumps10from simplejson import dumps
11import transaction11import transaction
12from zope.component import getUtility12from zope.component import getUtility
13from zope.security.proxy import removeSecurityProxy
1314
14from lp.answers.errors import (15from lp.answers.errors import (
15 AddAnswerContactError,16 AddAnswerContactError,
@@ -83,6 +84,7 @@
83 with celebrity_logged_in('admin'):84 with celebrity_logged_in('admin'):
84 self.question = self.factory.makeQuestion(85 self.question = self.factory.makeQuestion(
85 title="This is a question")86 title="This is a question")
87 self.target_name = self.question.target.name
8688
87 self.webservice = LaunchpadWebServiceCaller(89 self.webservice = LaunchpadWebServiceCaller(
88 'launchpad-library', 'salgado-change-anything')90 'launchpad-library', 'salgado-change-anything')
@@ -105,7 +107,7 @@
105 def test_GET_xhtml_representation(self):107 def test_GET_xhtml_representation(self):
106 # A question's xhtml representation is available on the api.108 # A question's xhtml representation is available on the api.
107 response = self.webservice.get(109 response = self.webservice.get(
108 '/%s/+question/%d' % (self.question.target.name,110 '/%s/+question/%d' % (self.target_name,
109 self.question.id),111 self.question.id),
110 'application/xhtml+xml')112 'application/xhtml+xml')
111 self.assertEqual(response.status, 200)113 self.assertEqual(response.status, 200)
@@ -119,7 +121,7 @@
119 new_title = "No, this is a question"121 new_title = "No, this is a question"
120122
121 question_json = self.webservice.get(123 question_json = self.webservice.get(
122 '/%s/+question/%d' % (self.question.target.name,124 '/%s/+question/%d' % (self.target_name,
123 self.question.id)).jsonBody()125 self.question.id)).jsonBody()
124126
125 response = self.webservice.patch(127 response = self.webservice.patch(
@@ -156,7 +158,7 @@
156 # End any open lplib instance.158 # End any open lplib instance.
157 logout()159 logout()
158 lp = launchpadlib_for("test", user)160 lp = launchpadlib_for("test", user)
159 return ws_object(lp, self.question)161 return ws_object(lp, removeSecurityProxy(self.question))
160162
161 def _set_visibility(self, question):163 def _set_visibility(self, question):
162 """Method to set visibility; needed for assertRaises."""164 """Method to set visibility; needed for assertRaises."""
163165
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2012-07-09 13:18:34 +0000
+++ lib/lp/app/browser/launchpad.py 2012-10-15 15:39:22 +0000
@@ -105,9 +105,11 @@
105from lp.registry.interfaces.pillar import IPillarNameSet105from lp.registry.interfaces.pillar import IPillarNameSet
106from lp.registry.interfaces.product import (106from lp.registry.interfaces.product import (
107 InvalidProductName,107 InvalidProductName,
108 IProduct,
108 IProductSet,109 IProductSet,
109 )110 )
110from lp.registry.interfaces.projectgroup import IProjectGroupSet111from lp.registry.interfaces.projectgroup import IProjectGroupSet
112from lp.registry.interfaces.role import IPersonRoles
111from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet113from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
112from lp.services.config import config114from lp.services.config import config
113from lp.services.helpers import intOrZero115from lp.services.helpers import intOrZero
@@ -763,6 +765,29 @@
763765
764 pillar = getUtility(IPillarNameSet).getByName(766 pillar = getUtility(IPillarNameSet).getByName(
765 name, ignore_inactive=False)767 name, ignore_inactive=False)
768
769 if (pillar is not None and IProduct.providedBy(pillar) and
770 not pillar.active):
771 # Emergency brake for public but inactive products:
772 # These products should not be shown to ordinary users.
773 # The root problem is that many views iterate over products,
774 # inactive products included, and access attributes like
775 # name, displayname or call canonical_url(product) --
776 # and finally throw the data away, if the product is
777 # inactive. So we cannot make these attributes inaccessible
778 # for inactive public products. On the other hand, we
779 # require the permission launchpad.View to protect private
780 # products.
781 # This means that we cannot simply check if the current
782 # user has the permission launchpad.View for an inactive
783 # product.
784 user = getUtility(ILaunchBag).user
785 if user is None:
786 return None
787 user = IPersonRoles(user)
788 if (not user.in_commercial_admin and not user.in_admin and
789 not user.in_registry_experts):
790 return None
766 if pillar is not None and check_permission('launchpad.View', pillar):791 if pillar is not None and check_permission('launchpad.View', pillar):
767 if pillar.name != name:792 if pillar.name != name:
768 # This pillar was accessed through one of its aliases, so we793 # This pillar was accessed through one of its aliases, so we
769794
=== modified file 'lib/lp/app/browser/tests/test_launchpad.py'
--- lib/lp/app/browser/tests/test_launchpad.py 2012-09-17 15:19:10 +0000
+++ lib/lp/app/browser/tests/test_launchpad.py 2012-10-15 15:39:22 +0000
@@ -20,8 +20,12 @@
20from lp.app.enums import InformationType20from lp.app.enums import InformationType
21from lp.app.errors import GoneError21from lp.app.errors import GoneError
22from lp.app.interfaces.launchpad import ILaunchpadCelebrities22from lp.app.interfaces.launchpad import ILaunchpadCelebrities
23from lp.app.interfaces.services import IService
23from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch24from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
24from lp.registry.enums import PersonVisibility25from lp.registry.enums import (
26 PersonVisibility,
27 SharingPermission,
28 )
25from lp.registry.interfaces.person import IPersonSet29from lp.registry.interfaces.person import IPersonSet
26from lp.services.identity.interfaces.account import AccountStatus30from lp.services.identity.interfaces.account import AccountStatus
27from lp.services.webapp import canonical_url31from lp.services.webapp import canonical_url
@@ -33,6 +37,7 @@
33from lp.services.webapp.url import urlappend37from lp.services.webapp.url import urlappend
34from lp.testing import (38from lp.testing import (
35 ANONYMOUS,39 ANONYMOUS,
40 celebrity_logged_in,
36 login,41 login,
37 login_person,42 login_person,
38 person_logged_in,43 person_logged_in,
@@ -468,3 +473,101 @@
468 reg.name for reg in iter_view_registrations(macros.__class__))473 reg.name for reg in iter_view_registrations(macros.__class__))
469 self.assertIn('+base-layout-macros', names)474 self.assertIn('+base-layout-macros', names)
470 self.assertNotIn('+related-pages', names)475 self.assertNotIn('+related-pages', names)
476
477
478class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
479
480 layer = DatabaseFunctionalLayer
481
482 def setUp(self):
483 super(TestProductTraversal, self).setUp()
484 self.active_public_product = self.factory.makeProduct()
485 self.inactive_public_product = self.factory.makeProduct()
486 removeSecurityProxy(self.inactive_public_product).active = False
487 self.proprietary_product_owner = self.factory.makePerson()
488 self.active_proprietary_product = self.factory.makeProduct(
489 owner=self.proprietary_product_owner,
490 information_type=InformationType.PROPRIETARY)
491 self.inactive_proprietary_product = self.factory.makeProduct(
492 owner=self.proprietary_product_owner,
493 information_type=InformationType.PROPRIETARY)
494 removeSecurityProxy(self.inactive_proprietary_product).active = False
495
496 def traverse_to_active_public_product(self):
497 segment = self.active_public_product.name
498 self.traverse(segment, segment)
499
500 def traverse_to_inactive_public_product(self):
501 segment = removeSecurityProxy(self.inactive_public_product).name
502 self.traverse(segment, segment)
503
504 def traverse_to_active_proprietary_product(self):
505 segment = removeSecurityProxy(self.active_proprietary_product).name
506 self.traverse(segment, segment)
507
508 def traverse_to_inactive_proprietary_product(self):
509 segment = removeSecurityProxy(self.inactive_proprietary_product).name
510 self.traverse(segment, segment)
511
512 def test_access_for_anon(self):
513 # Anonymous users can see only public active products.
514 with person_logged_in(ANONYMOUS):
515 self.traverse_to_active_public_product()
516 # Access to other products raises a NotFound error.
517 self.assertRaises(
518 NotFound, self.traverse_to_inactive_public_product)
519 self.assertRaises(
520 NotFound, self.traverse_to_active_proprietary_product)
521 self.assertRaises(
522 NotFound, self.traverse_to_inactive_proprietary_product)
523
524 def test_access_for_ordinary_users(self):
525 # Ordinary logged in users can see only public active products.
526 with person_logged_in(self.factory.makePerson()):
527 self.traverse_to_active_public_product()
528 # Access to other products raises a NotFound error.
529 self.assertRaises(
530 NotFound, self.traverse_to_inactive_public_product)
531 self.assertRaises(
532 NotFound, self.traverse_to_active_proprietary_product)
533 self.assertRaises(
534 NotFound, self.traverse_to_inactive_proprietary_product)
535
536 def test_access_for_person_with_pillar_grant(self):
537 # Persons with a policy grant for a proprietary product can
538 # access this product, if it is active.
539 user = self.factory.makePerson()
540 with person_logged_in(self.proprietary_product_owner):
541 getUtility(IService, 'sharing').sharePillarInformation(
542 self.active_proprietary_product, user,
543 self.proprietary_product_owner,
544 {InformationType.PROPRIETARY: SharingPermission.ALL})
545 getUtility(IService, 'sharing').sharePillarInformation(
546 self.inactive_proprietary_product, user,
547 self.proprietary_product_owner,
548 {InformationType.PROPRIETARY: SharingPermission.ALL})
549 with person_logged_in(user):
550 self.traverse_to_active_public_product()
551 self.assertRaises(
552 NotFound, self.traverse_to_inactive_public_product)
553 self.traverse_to_active_proprietary_product()
554 self.assertRaises(
555 NotFound, self.traverse_to_inactive_proprietary_product)
556
557 def check_admin_access(self):
558 self.traverse_to_active_public_product()
559 self.traverse_to_inactive_public_product()
560 self.traverse_to_active_proprietary_product()
561 self.traverse_to_inactive_proprietary_product()
562
563 def test_access_for_persons_with_special_permissions(self):
564 # Admins have access all products, including inactive and propretary
565 # products.
566 with celebrity_logged_in('admin'):
567 self.check_admin_access()
568 # Registry experts can access to all products.
569 with celebrity_logged_in('registry_experts'):
570 self.check_admin_access()
571 # Commercial admins have access to all products.
572 with celebrity_logged_in('commercial_admin'):
573 self.check_admin_access()
471574
=== modified file 'lib/lp/app/model/launchpad.py'
--- lib/lp/app/model/launchpad.py 2012-10-08 10:07:11 +0000
+++ lib/lp/app/model/launchpad.py 2012-10-15 15:39:22 +0000
@@ -43,7 +43,7 @@
4343
4444
45class InformationTypeMixin:45class InformationTypeMixin:
46 """"Common functionality for classes implementing IInformationType."""46 """Common functionality for classes implementing IInformationType."""
4747
48 @property48 @property
49 def private(self):49 def private(self):
5050
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
--- lib/lp/blueprints/browser/tests/test_specification.py 2012-10-11 12:41:43 +0000
+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-10-15 15:39:22 +0000
@@ -40,6 +40,7 @@
40 IProductSeries,40 IProductSeries,
41 )41 )
42from lp.services.features.testing import FeatureFixture42from lp.services.features.testing import FeatureFixture
43from lp.services.webapp.interaction import ANONYMOUS
43from lp.services.webapp.interfaces import BrowserNotificationLevel44from lp.services.webapp.interfaces import BrowserNotificationLevel
44from lp.services.webapp.publisher import canonical_url45from lp.services.webapp.publisher import canonical_url
45from lp.testing import (46from lp.testing import (
@@ -480,7 +481,11 @@
480 control = browser.getControl(information_type.title)481 control = browser.getControl(information_type.title)
481 if not control.selected:482 if not control.selected:
482 control.click()483 control.click()
483 return product.getSpecification(self.submitSpec(browser))484 specification_name = self.submitSpec(browser)
485 # Using the browser terminated the interaction, but we need
486 # an interaction in order to access a product.
487 with person_logged_in(ANONYMOUS):
488 return product.getSpecification(specification_name)
484489
485 def test_supplied_information_types(self):490 def test_supplied_information_types(self):
486 """Creating honours information types."""491 """Creating honours information types."""
@@ -564,9 +569,11 @@
564 Useful because we need to follow to product from a569 Useful because we need to follow to product from a
565 ProductSeries.570 ProductSeries.
566 """571 """
567 if IProductSeries.providedBy(target):572 # We need an interaction in order to access a product.
568 return target.product.getSpecification(name)573 with person_logged_in(ANONYMOUS):
569 return target.getSpecification(name)574 if IProductSeries.providedBy(target):
575 return target.product.getSpecification(name)
576 return target.getSpecification(name)
570577
571 def submitSpec(self, browser):578 def submitSpec(self, browser):
572 """Submit a Specification via a browser."""579 """Submit a Specification via a browser."""
573580
=== modified file 'lib/lp/blueprints/browser/tests/test_views.py'
--- lib/lp/blueprints/browser/tests/test_views.py 2012-10-08 10:07:11 +0000
+++ lib/lp/blueprints/browser/tests/test_views.py 2012-10-15 15:39:22 +0000
@@ -66,9 +66,9 @@
66 collector = QueryCollector()66 collector = QueryCollector()
67 collector.register()67 collector.register()
68 self.addCleanup(collector.unregister)68 self.addCleanup(collector.unregister)
69 url = canonical_url(target) + "/+assignments"
69 viewer = self.factory.makePerson()70 viewer = self.factory.makePerson()
70 browser = self.getUserBrowser(user=viewer)71 browser = self.getUserBrowser(user=viewer)
71 url = canonical_url(target) + "/+assignments"
72 # Seed the cookie cache and any other cross-request state we may gain72 # Seed the cookie cache and any other cross-request state we may gain
73 # in future. See lp.services.webapp.serssion: _get_secret.73 # in future. See lp.services.webapp.serssion: _get_secret.
74 browser.open(url)74 browser.open(url)
7575
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 2012-10-08 10:07:11 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2012-10-15 15:39:22 +0000
@@ -7,6 +7,7 @@
77
8import transaction8import transaction
9from zope.security.management import endInteraction9from zope.security.management import endInteraction
10from zope.security.proxy import removeSecurityProxy
1011
11from lp.blueprints.enums import SpecificationDefinitionStatus12from lp.blueprints.enums import SpecificationDefinitionStatus
12from lp.services.webapp.interaction import ANONYMOUS13from lp.services.webapp.interaction import ANONYMOUS
@@ -42,8 +43,9 @@
42 return result43 return result
4344
44 def getPillarOnWebservice(self, pillar_obj):45 def getPillarOnWebservice(self, pillar_obj):
46 pillar_name = pillar_obj.name
45 launchpadlib = self.getLaunchpadlib()47 launchpadlib = self.getLaunchpadlib()
46 return launchpadlib.load(pillar_obj.name)48 return launchpadlib.load(pillar_name)
4749
4850
49class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):51class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
@@ -74,9 +76,9 @@
74 def test_representation_contains_target(self):76 def test_representation_contains_target(self):
75 spec = self.factory.makeSpecification(77 spec = self.factory.makeSpecification(
76 product=self.factory.makeProduct())78 product=self.factory.makeProduct())
77 spec_target = spec.target79 spec_target_name = spec.target.name
78 spec_webservice = self.getSpecOnWebservice(spec)80 spec_webservice = self.getSpecOnWebservice(spec)
79 self.assertEqual(spec_target.name, spec_webservice.target.name)81 self.assertEqual(spec_target_name, spec_webservice.target.name)
8082
81 def test_representation_contains_title(self):83 def test_representation_contains_title(self):
82 spec = self.factory.makeSpecification(title='Foo')84 spec = self.factory.makeSpecification(title='Foo')
@@ -271,7 +273,7 @@
271 # setup a new one.273 # setup a new one.
272 endInteraction()274 endInteraction()
273 lplib = launchpadlib_for('lplib-test', person=None, version='devel')275 lplib = launchpadlib_for('lplib-test', person=None, version='devel')
274 ws_product = ws_object(lplib, product)276 ws_product = ws_object(lplib, removeSecurityProxy(product))
275 self.assertNamesOfSpecificationsAre(277 self.assertNamesOfSpecificationsAre(
276 ["spec1", "spec2"], ws_product.all_specifications)278 ["spec1", "spec2"], ws_product.all_specifications)
277279
278280
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-15 02:32:30 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-15 15:39:22 +0000
@@ -2024,8 +2024,10 @@
20242024
2025 def test_rendered_query_counts_constant_with_many_bugtasks(self):2025 def test_rendered_query_counts_constant_with_many_bugtasks(self):
2026 product = self.factory.makeProduct()2026 product = self.factory.makeProduct()
2027 url = canonical_url(product, view_name='+bugs')
2027 bug = self.factory.makeBug(target=product)2028 bug = self.factory.makeBug(target=product)
2028 buggy_product = self.factory.makeProduct()2029 buggy_product = self.factory.makeProduct()
2030 buggy_url = canonical_url(buggy_product, view_name='+bugs')
2029 for _ in range(10):2031 for _ in range(10):
2030 self.factory.makeBug(target=buggy_product)2032 self.factory.makeBug(target=buggy_product)
2031 recorder = QueryCollector()2033 recorder = QueryCollector()
@@ -2033,11 +2035,9 @@
2033 self.addCleanup(recorder.unregister)2035 self.addCleanup(recorder.unregister)
2034 self.invalidate_caches(bug)2036 self.invalidate_caches(bug)
2035 # count with single task2037 # count with single task
2036 url = canonical_url(product, view_name='+bugs')
2037 self.getUserBrowser(url)2038 self.getUserBrowser(url)
2038 self.assertThat(recorder, HasQueryCount(LessThan(35)))2039 self.assertThat(recorder, HasQueryCount(LessThan(35)))
2039 # count with many tasks2040 # count with many tasks
2040 buggy_url = canonical_url(buggy_product, view_name='+bugs')
2041 self.getUserBrowser(buggy_url)2041 self.getUserBrowser(buggy_url)
2042 self.assertThat(recorder, HasQueryCount(LessThan(35)))2042 self.assertThat(recorder, HasQueryCount(LessThan(35)))
20432043
20442044
=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-10-15 15:39:22 +0000
@@ -219,6 +219,7 @@
219 >>> other_product = factory.makeProduct(219 >>> other_product = factory.makeProduct(
220 ... official_malone=True,220 ... official_malone=True,
221 ... bug_sharing_policy=BugSharingPolicy.PROPRIETARY)221 ... bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
222 >>> other_product_name = other_product.name
222 >>> params = CreateBugParams(223 >>> params = CreateBugParams(
223 ... title="a test private bug",224 ... title="a test private bug",
224 ... comment="a description of the bug",225 ... comment="a description of the bug",
@@ -229,7 +230,7 @@
229230
230 >>> browser.open(canonical_url(private_bug, rootsite='bugs'))231 >>> browser.open(canonical_url(private_bug, rootsite='bugs'))
231 >>> browser.getLink(url='+choose-affected-product').click()232 >>> browser.getLink(url='+choose-affected-product').click()
232 >>> browser.getControl(name='field.product').value = other_product.name233 >>> browser.getControl(name='field.product').value = other_product_name
233 >>> browser.getControl('Continue').click()234 >>> browser.getControl('Continue').click()
234 >>> print browser.url235 >>> print browser.url
235 http://bugs.launchpad.dev/proprietary-product/+bug/16/+choose-affected-product236 http://bugs.launchpad.dev/proprietary-product/+bug/16/+choose-affected-product
236237
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-10-15 15:39:22 +0000
@@ -9,8 +9,9 @@
9 >>> login(USER_EMAIL)9 >>> login(USER_EMAIL)
10 >>> bug = factory.makeBug()10 >>> bug = factory.makeBug()
11 >>> task = bug.default_bugtask11 >>> task = bug.default_bugtask
12 >>> url = canonical_url(task, view_name='+subscribe')
12 >>> logout()13 >>> logout()
13 >>> user_browser.open(canonical_url(task, view_name='+subscribe'))14 >>> user_browser.open(url)
14 >>> bug_notification_level_control = user_browser.getControl(15 >>> bug_notification_level_control = user_browser.getControl(
15 ... name='field.bug_notification_level')16 ... name='field.bug_notification_level')
16 >>> for control in bug_notification_level_control.controls:17 >>> for control in bug_notification_level_control.controls:
1718
=== modified file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
--- lib/lp/bugs/stories/patches-view/patches-view.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/stories/patches-view/patches-view.txt 2012-10-15 15:39:22 +0000
@@ -255,9 +255,11 @@
255 ... bugtask.transitionToImportance(importance, ubuntu_distro.owner)255 ... bugtask.transitionToImportance(importance, ubuntu_distro.owner)
256 ... if status is not None:256 ... if status is not None:
257 ... bugtask.transitionToStatus(status, ubuntu_distro.owner)257 ... bugtask.transitionToStatus(status, ubuntu_distro.owner)
258 >>> login(ANONYMOUS)
258 >>> patchy_product_series = patchy_product.getSeries('trunk')259 >>> patchy_product_series = patchy_product.getSeries('trunk')
259 >>> make_bugtask(bug=bug_a, target=patchy_product_series)260 >>> make_bugtask(bug=bug_a, target=patchy_product_series)
260 >>> make_bugtask(bug=bug_c, target=patchy_product_series)261 >>> make_bugtask(bug=bug_c, target=patchy_product_series)
262 >>> logout()
261 >>> anon_browser.open(263 >>> anon_browser.open(
262 ... 'https://bugs.launchpad.dev/patchy-product-1/trunk/+patches')264 ... 'https://bugs.launchpad.dev/patchy-product-1/trunk/+patches')
263 >>> show_patches_view(anon_browser.contents)265 >>> show_patches_view(anon_browser.contents)
264266
=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt'
--- lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2012-10-15 15:39:22 +0000
@@ -8,8 +8,8 @@
8 >>> from lp.testing.sampledata import USER_EMAIL8 >>> from lp.testing.sampledata import USER_EMAIL
9 >>> login(USER_EMAIL)9 >>> login(USER_EMAIL)
10 >>> product = factory.makeProduct()10 >>> product = factory.makeProduct()
11 >>> url = canonical_url(product, view_name='+subscriptions')
11 >>> logout()12 >>> logout()
12 >>> user_browser.open(13 >>> user_browser.open(url)
13 ... canonical_url(product, view_name='+subscriptions'))
14 >>> user_browser.getLink("Add a subscription")14 >>> user_browser.getLink("Add a subscription")
15 <Link text='Add a subscription' url='.../+subscriptions#'>15 <Link text='Add a subscription' url='.../+subscriptions#'>
1616
=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
--- lib/lp/bugs/stories/webservice/xx-bug.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2012-10-15 15:39:22 +0000
@@ -1463,13 +1463,14 @@
1463 >>> from lp.testing.sampledata import ADMIN_EMAIL1463 >>> from lp.testing.sampledata import ADMIN_EMAIL
1464 >>> login(ADMIN_EMAIL)1464 >>> login(ADMIN_EMAIL)
1465 >>> target = factory.makeProduct()1465 >>> target = factory.makeProduct()
1466 >>> target_name = target.name
1466 >>> bug = factory.makeBug(target=target)1467 >>> bug = factory.makeBug(target=target)
1467 >>> bug = removeSecurityProxy(bug)1468 >>> bug = removeSecurityProxy(bug)
1468 >>> date = bug.date_last_updated - timedelta(days=6)1469 >>> date = bug.date_last_updated - timedelta(days=6)
1469 >>> logout()1470 >>> logout()
14701471
1471 >>> pprint_collection(webservice.named_get(1472 >>> pprint_collection(webservice.named_get(
1472 ... '/%s' % target.name, 'searchTasks',1473 ... '/%s' % target_name, 'searchTasks',
1473 ... modified_since=u'%s' % date ).jsonBody())1474 ... modified_since=u'%s' % date ).jsonBody())
1474 start: 01475 start: 0
1475 total_size: 11476 total_size: 1
@@ -1481,12 +1482,13 @@
1481 >>> from lp.bugs.interfaces.bugtarget import IBugTarget1482 >>> from lp.bugs.interfaces.bugtarget import IBugTarget
1482 >>> login(ADMIN_EMAIL)1483 >>> login(ADMIN_EMAIL)
1483 >>> target = IBugTarget(factory.makeProduct())1484 >>> target = IBugTarget(factory.makeProduct())
1485 >>> target_name = target.name
1484 >>> task = factory.makeBugTask(target=target)1486 >>> task = factory.makeBugTask(target=target)
1485 >>> date = task.datecreated - timedelta(days=8)1487 >>> date = task.datecreated - timedelta(days=8)
1486 >>> logout()1488 >>> logout()
14871489
1488 >>> pprint_collection(webservice.named_get(1490 >>> pprint_collection(webservice.named_get(
1489 ... '/%s' % target.name, 'searchTasks',1491 ... '/%s' % target_name, 'searchTasks',
1490 ... created_since=u'%s' % date).jsonBody())1492 ... created_since=u'%s' % date).jsonBody())
1491 start: 01493 start: 0
1492 total_size: 11494 total_size: 1
@@ -1497,7 +1499,7 @@
14971499
1498 >>> before_date = task.datecreated + timedelta(days=8)1500 >>> before_date = task.datecreated + timedelta(days=8)
1499 >>> pprint_collection(webservice.named_get(1501 >>> pprint_collection(webservice.named_get(
1500 ... '/%s' % target.name, 'searchTasks',1502 ... '/%s' % target_name, 'searchTasks',
1501 ... created_before=u'%s' % before_date).jsonBody())1503 ... created_before=u'%s' % before_date).jsonBody())
1502 start: 01504 start: 0
1503 total_size: 11505 total_size: 1
15041506
=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
--- lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-09 08:39:54 +0000
+++ lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-15 15:39:22 +0000
@@ -355,12 +355,13 @@
355 def test_add_duplicate_bugtask_for_project_gives_bad_request(self):355 def test_add_duplicate_bugtask_for_project_gives_bad_request(self):
356 bug = self.factory.makeBug()356 bug = self.factory.makeBug()
357 product = self.factory.makeProduct()357 product = self.factory.makeProduct()
358 product_url = api_url(product)
358 self.factory.makeBugTask(bug=bug, target=product)359 self.factory.makeBugTask(bug=bug, target=product)
359360
360 launchpad = launchpadlib_for('test', bug.owner)361 launchpad = launchpadlib_for('test', bug.owner)
361 lp_bug = launchpad.load(api_url(bug))362 lp_bug = launchpad.load(api_url(bug))
362 self.assertRaises(363 self.assertRaises(
363 BadRequest, lp_bug.addTask, target=api_url(product))364 BadRequest, lp_bug.addTask, target=product_url)
364365
365 def test_add_invalid_bugtask_to_proprietary_bug_gives_bad_request(self):366 def test_add_invalid_bugtask_to_proprietary_bug_gives_bad_request(self):
366 # Test we get an error when we attempt to invalidly add a bug task to367 # Test we get an error when we attempt to invalidly add a bug task to
@@ -371,6 +372,7 @@
371 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)372 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
372 product2 = self.factory.makeProduct(373 product2 = self.factory.makeProduct(
373 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)374 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
375 product2_url = api_url(product2)
374 bug = self.factory.makeBug(376 bug = self.factory.makeBug(
375 target=product1, owner=owner,377 target=product1, owner=owner,
376 information_type=InformationType.PROPRIETARY)378 information_type=InformationType.PROPRIETARY)
@@ -379,7 +381,7 @@
379 launchpad = launchpadlib_for('test', owner)381 launchpad = launchpadlib_for('test', owner)
380 lp_bug = launchpad.load(api_url(bug))382 lp_bug = launchpad.load(api_url(bug))
381 self.assertRaises(383 self.assertRaises(
382 BadRequest, lp_bug.addTask, target=api_url(product2))384 BadRequest, lp_bug.addTask, target=product2_url)
383385
384 def test_add_attachment_with_bad_filename_raises_exception(self):386 def test_add_attachment_with_bad_filename_raises_exception(self):
385 # Test that addAttachment raises BadRequest when the filename given387 # Test that addAttachment raises BadRequest when the filename given
@@ -404,13 +406,14 @@
404 # to the project's bug sharing policy.406 # to the project's bug sharing policy.
405 project = self.factory.makeProduct(407 project = self.factory.makeProduct(
406 licenses=[License.OTHER_PROPRIETARY])408 licenses=[License.OTHER_PROPRIETARY])
409 target_url = api_url(project)
407 with person_logged_in(project.owner):410 with person_logged_in(project.owner):
408 project.setBugSharingPolicy(411 project.setBugSharingPolicy(
409 BugSharingPolicy.PROPRIETARY_OR_PUBLIC)412 BugSharingPolicy.PROPRIETARY_OR_PUBLIC)
410 webservice = launchpadlib_for('test', 'salgado')413 webservice = launchpadlib_for('test', 'salgado')
411 bugs_collection = webservice.load('/bugs')414 bugs_collection = webservice.load('/bugs')
412 bug = bugs_collection.createBug(415 bug = bugs_collection.createBug(
413 target=api_url(project), title='title', description='desc')416 target=target_url, title='title', description='desc')
414 self.assertEqual('Proprietary', bug.information_type)417 self.assertEqual('Proprietary', bug.information_type)
415418
416419
@@ -430,7 +433,7 @@
430433
431 def test_subscribe_does_not_update(self):434 def test_subscribe_does_not_update(self):
432 # Calling subscribe over the API does not update date_last_updated.435 # Calling subscribe over the API does not update date_last_updated.
433 (bug, owner, webservice) = self.make_old_bug()436 (bug, owner, webservice) = self.make_old_bug()
434 subscriber = self.factory.makePerson()437 subscriber = self.factory.makePerson()
435 date_last_updated = bug.date_last_updated438 date_last_updated = bug.date_last_updated
436 api_sub = api_url(subscriber)439 api_sub = api_url(subscriber)
437440
=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-10-08 10:07:11 +0000
+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-10-15 15:39:22 +0000
@@ -54,6 +54,7 @@
54 self.owner = self.factory.makePerson()54 self.owner = self.factory.makePerson()
55 with person_logged_in(self.owner):55 with person_logged_in(self.owner):
56 self.product = self.factory.makeProduct()56 self.product = self.factory.makeProduct()
57 self.product_name = self.product.name
57 self.bug = self.factory.makeBug(58 self.bug = self.factory.makeBug(
58 target=self.product,59 target=self.product,
59 information_type=InformationType.PRIVATESECURITY)60 information_type=InformationType.PRIVATESECURITY)
@@ -62,7 +63,7 @@
6263
63 def search(self, api_version, **kwargs):64 def search(self, api_version, **kwargs):
64 return self.webservice.named_get(65 return self.webservice.named_get(
65 '/%s' % self.product.name, 'searchTasks',66 '/%s' % self.product_name, 'searchTasks',
66 api_version=api_version, **kwargs).jsonBody()67 api_version=api_version, **kwargs).jsonBody()
6768
68 def test_linked_blueprints_in_devel(self):69 def test_linked_blueprints_in_devel(self):
@@ -102,7 +103,7 @@
102 def test_search_with_wrong_orderby(self):103 def test_search_with_wrong_orderby(self):
103 # Calling searchTasks() with a wrong order_by is a Bad Request.104 # Calling searchTasks() with a wrong order_by is a Bad Request.
104 response = self.webservice.named_get(105 response = self.webservice.named_get(
105 '/%s' % self.product.name, 'searchTasks',106 '/%s' % self.product_name, 'searchTasks',
106 api_version='devel', order_by='date_created')107 api_version='devel', order_by='date_created')
107 self.assertEqual(400, response.status)108 self.assertEqual(400, response.status)
108 self.assertRaisesWithContent(109 self.assertRaisesWithContent(
109110
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2012-10-09 00:05:04 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2012-10-15 15:39:22 +0000
@@ -694,11 +694,11 @@
694 base_url = canonical_url(branch, rootsite='code')694 base_url = canonical_url(branch, rootsite='code')
695 product_url = canonical_url(product, rootsite='code')695 product_url = canonical_url(product, rootsite='code')
696 url = '%s/+subscription/%s' % (base_url, subscriber.name)696 url = '%s/+subscription/%s' % (base_url, subscriber.name)
697 expected_title = "Code : %s" % product.displayname
697 browser = self._getBrowser(user=subscriber)698 browser = self._getBrowser(user=subscriber)
698 browser.open(url)699 browser.open(url)
699 browser.getControl('Unsubscribe').click()700 browser.getControl('Unsubscribe').click()
700 self.assertEqual(product_url, browser.url)701 self.assertEqual(product_url, browser.url)
701 expected_title = "Code : %s" % product.displayname
702 self.assertEqual(expected_title, browser.title)702 self.assertEqual(expected_title, browser.title)
703703
704704
705705
=== modified file 'lib/lp/code/browser/tests/test_product.py'
--- lib/lp/code/browser/tests/test_product.py 2012-10-08 14:27:31 +0000
+++ lib/lp/code/browser/tests/test_product.py 2012-10-15 15:39:22 +0000
@@ -222,8 +222,9 @@
222 login_person(product.owner)222 login_person(product.owner)
223 product.development_focus.branch = code_import.branch223 product.development_focus.branch = code_import.branch
224 self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)224 self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
225 product_url = canonical_url(product, rootsite='code')
225 logout()226 logout()
226 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))227 browser = self.getUserBrowser(product_url)
227 login(ANONYMOUS)228 login(ANONYMOUS)
228 content = find_tag_by_id(browser.contents, 'external')229 content = find_tag_by_id(browser.contents, 'external')
229 text = extract_text(content)230 text = extract_text(content)
@@ -379,6 +380,7 @@
379380
380 def test_is_public(self):381 def test_is_public(self):
381 product = self.factory.makeProduct()382 product = self.factory.makeProduct()
383 product_displayname = product.displayname
382 branch = self.factory.makeProductBranch(product=product)384 branch = self.factory.makeProductBranch(product=product)
383 login_person(product.owner)385 login_person(product.owner)
384 product.development_focus.branch = branch386 product.development_focus.branch = branch
@@ -386,7 +388,7 @@
386 text = extract_text(find_tag_by_id(browser.contents, 'privacy'))388 text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
387 expected = (389 expected = (
388 "New branches for %(name)s are Public.*"390 "New branches for %(name)s are Public.*"
389 % dict(name=product.displayname))391 % dict(name=product_displayname))
390 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)392 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
391393
392394
393395
=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
--- lib/lp/code/stories/branches/xx-product-branches.txt 2012-10-11 04:14:37 +0000
+++ lib/lp/code/stories/branches/xx-product-branches.txt 2012-10-15 15:39:22 +0000
@@ -180,10 +180,10 @@
180 >>> factory = LaunchpadObjectFactory()180 >>> factory = LaunchpadObjectFactory()
181 >>> login(ANONYMOUS)181 >>> login(ANONYMOUS)
182 >>> product = getUtility(IProductSet).getByName('firefox')182 >>> product = getUtility(IProductSet).getByName('firefox')
183 >>> owner = product.owner
183 >>> old_branch = product.development_focus.branch184 >>> old_branch = product.development_focus.branch
184 >>> ignored = login_person(product.owner)185 >>> ignored = login_person(product.owner)
185 >>> product.development_focus.branch = None186 >>> product.development_focus.branch = None
186 >>> logout()
187 >>> def print_links(browser):187 >>> def print_links(browser):
188 ... links = find_tag_by_id(browser.contents, 'involvement')188 ... links = find_tag_by_id(browser.contents, 'involvement')
189 ... if links is None:189 ... if links is None:
@@ -203,13 +203,16 @@
203203
204 >>> print product.codehosting_usage.name204 >>> print product.codehosting_usage.name
205 UNKNOWN205 UNKNOWN
206 >>> logout()
206 >>> admin_browser.open('http://code.launchpad.dev/firefox')207 >>> admin_browser.open('http://code.launchpad.dev/firefox')
207 >>> print_links(admin_browser)208 >>> print_links(admin_browser)
208 None209 None
209210
210 >>> setup_code_hosting('firefox')211 >>> setup_code_hosting('firefox')
212 >>> login(ANONYMOUS)
211 >>> print product.codehosting_usage.name213 >>> print product.codehosting_usage.name
212 LAUNCHPAD214 LAUNCHPAD
215 >>> logout()
213 >>> admin_browser.open('http://code.launchpad.dev/firefox')216 >>> admin_browser.open('http://code.launchpad.dev/firefox')
214 >>> print_links(admin_browser)217 >>> print_links(admin_browser)
215 Import a branch218 Import a branch
@@ -233,7 +236,7 @@
233If the product specifies that it officially uses Launchpad code, then236If the product specifies that it officially uses Launchpad code, then
234the 'Import a branch' button is still shown.237the 'Import a branch' button is still shown.
235238
236 >>> ignored = login_person(product.owner)239 >>> ignored = login_person(owner)
237 >>> product.development_focus.branch = old_branch240 >>> product.development_focus.branch = old_branch
238 >>> logout()241 >>> logout()
239 >>> browser.open('http://code.launchpad.dev/firefox')242 >>> browser.open('http://code.launchpad.dev/firefox')
240243
=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
--- lib/lp/code/stories/webservice/xx-code-import.txt 2012-10-09 00:10:04 +0000
+++ lib/lp/code/stories/webservice/xx-code-import.txt 2012-10-15 15:39:22 +0000
@@ -17,6 +17,7 @@
17 >>> other_person = factory.makePerson(name='other-person')17 >>> other_person = factory.makePerson(name='other-person')
18 >>> removeSecurityProxy(person).join(team)18 >>> removeSecurityProxy(person).join(team)
19 >>> product = factory.makeProduct(name='scruff')19 >>> product = factory.makeProduct(name='scruff')
20 >>> product_name = product.name
20 >>> svn_branch_url = "http://svn.domain.com/source"21 >>> svn_branch_url = "http://svn.domain.com/source"
21 >>> code_import = removeSecurityProxy(factory.makeProductCodeImport(22 >>> code_import = removeSecurityProxy(factory.makeProductCodeImport(
22 ... registrant=person, product=product, branch_name='import',23 ... registrant=person, product=product, branch_name='import',
@@ -122,7 +123,7 @@
122123
123We can create an import using the API by calling a method on the project.124We can create an import using the API by calling a method on the project.
124125
125 >>> product_url = '/' + product.name126 >>> product_url = '/' + product_name
126 >>> new_remote_url = factory.getUniqueURL()127 >>> new_remote_url = factory.getUniqueURL()
127 >>> response = import_webservice.named_post(product_url, 'newCodeImport',128 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
128 ... branch_name='new-import', rcs_type='Git',129 ... branch_name='new-import', rcs_type='Git',
@@ -149,7 +150,7 @@
149150
150If we must we can create a CVS import.151If we must we can create a CVS import.
151152
152 >>> product_url = '/' + product.name153 >>> product_url = '/' + product_name
153 >>> new_remote_url = factory.getUniqueURL()154 >>> new_remote_url = factory.getUniqueURL()
154 >>> response = import_webservice.named_post(product_url, 'newCodeImport',155 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
155 ... branch_name='cvs-import', rcs_type='Concurrent Versions System',156 ... branch_name='cvs-import', rcs_type='Concurrent Versions System',
156157
=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py 2012-10-15 15:39:22 +0000
@@ -234,12 +234,13 @@
234 super(TestProjectMilestoneIndexQueryCount, self).setUp()234 super(TestProjectMilestoneIndexQueryCount, self).setUp()
235 self.owner = self.factory.makePerson(name='product-owner')235 self.owner = self.factory.makePerson(name='product-owner')
236 self.product = self.factory.makeProduct(owner=self.owner)236 self.product = self.factory.makeProduct(owner=self.owner)
237 self.product_owner = self.product.owner
237 login_person(self.product.owner)238 login_person(self.product.owner)
238 self.milestone = self.factory.makeMilestone(239 self.milestone = self.factory.makeMilestone(
239 productseries=self.product.development_focus)240 productseries=self.product.development_focus)
240241
241 def add_bug(self, count):242 def add_bug(self, count):
242 login_person(self.product.owner)243 login_person(self.product_owner)
243 for i in range(count):244 for i in range(count):
244 bug = self.factory.makeBug(target=self.product)245 bug = self.factory.makeBug(target=self.product)
245 bug.bugtasks[0].transitionToMilestone(246 bug.bugtasks[0].transitionToMilestone(
@@ -284,6 +285,7 @@
284 # increasing the cap.285 # increasing the cap.
285 page_query_limit = 37286 page_query_limit = 37
286 product = self.factory.makeProduct()287 product = self.factory.makeProduct()
288 product_owner = product.owner
287 login_person(product.owner)289 login_person(product.owner)
288 milestone = self.factory.makeMilestone(290 milestone = self.factory.makeMilestone(
289 productseries=product.development_focus)291 productseries=product.development_focus)
@@ -316,7 +318,7 @@
316 with_1_private_bug = collector.count318 with_1_private_bug = collector.count
317 with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in319 with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in
318 enumerate(collector.queries)]320 enumerate(collector.queries)]
319 login_person(product.owner)321 login_person(product_owner)
320 bug2 = self.factory.makeBug(322 bug2 = self.factory.makeBug(
321 target=product, information_type=InformationType.USERDATA,323 target=product, information_type=InformationType.USERDATA,
322 owner=product.owner)324 owner=product.owner)
323325
=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-10-15 15:39:22 +0000
@@ -243,10 +243,10 @@
243243
244 def test_sharing_menu(self):244 def test_sharing_menu(self):
245 url = canonical_url(self.pillar)245 url = canonical_url(self.pillar)
246 browser = setupBrowserForUser(user=self.driver)
247 browser.open(url)
248 soup = BeautifulSoup(browser.contents)
249 sharing_url = canonical_url(self.pillar, view_name='+sharing')246 sharing_url = canonical_url(self.pillar, view_name='+sharing')
247 browser = setupBrowserForUser(user=self.driver)
248 browser.open(url)
249 soup = BeautifulSoup(browser.contents)
250 sharing_menu = soup.find('a', {'href': sharing_url})250 sharing_menu = soup.find('a', {'href': sharing_url})
251 self.assertIsNotNone(sharing_menu)251 self.assertIsNotNone(sharing_menu)
252252
253253
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-10-12 20:17:12 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-10-15 15:39:22 +0000
@@ -631,8 +631,8 @@
631 def test_headers(self):631 def test_headers(self):
632 """The headers for the RDF view of a product should be as expected."""632 """The headers for the RDF view of a product should be as expected."""
633 product = self.factory.makeProduct()633 product = self.factory.makeProduct()
634 content_disposition = 'attachment; filename="%s.rdf"' % product.name
634 browser = self.getViewBrowser(product, view_name='+rdf')635 browser = self.getViewBrowser(product, view_name='+rdf')
635 content_disposition = 'attachment; filename="%s.rdf"' % product.name
636 self.assertEqual(636 self.assertEqual(
637 content_disposition, browser.headers['Content-disposition'])637 content_disposition, browser.headers['Content-disposition'])
638 self.assertEqual(638 self.assertEqual(
639639
=== modified file 'lib/lp/registry/browser/tests/test_productseries_views.py'
--- lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-11 16:27:29 +0000
+++ lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-15 15:39:22 +0000
@@ -8,12 +8,14 @@
88
9import soupmatchers9import soupmatchers
10from testtools.matchers import Not10from testtools.matchers import Not
11from zope.security.proxy import removeSecurityProxy
1112
12from lp.app.enums import InformationType13from lp.app.enums import InformationType
13from lp.bugs.interfaces.bugtask import (14from lp.bugs.interfaces.bugtask import (
14 BugTaskStatus,15 BugTaskStatus,
15 BugTaskStatusSearch,16 BugTaskStatusSearch,
16 )17 )
18from lp.services.webapp import canonical_url
17from lp.testing import (19from lp.testing import (
18 BrowserTestCase,20 BrowserTestCase,
19 person_logged_in,21 person_logged_in,
@@ -60,6 +62,11 @@
60 browser = self.getViewBrowser(series)62 browser = self.getViewBrowser(series)
61 self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))63 self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))
6264
65 def getBrowser(self, series, view_name=None):
66 series = removeSecurityProxy(series)
67 url = canonical_url(series, view_name=view_name)
68 return self.getUserBrowser(url, series.product.owner)
69
63 def test_package_proprietary_error(self):70 def test_package_proprietary_error(self):
64 """Packaging a proprietary product produces an error."""71 """Packaging a proprietary product produces an error."""
65 product = self.factory.makeProduct(72 product = self.factory.makeProduct(
@@ -68,7 +75,7 @@
68 ubuntu_series = self.factory.makeUbuntuDistroSeries()75 ubuntu_series = self.factory.makeUbuntuDistroSeries()
69 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series,76 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series,
70 publish=True)77 publish=True)
71 browser = self.getViewBrowser(productseries, '+ubuntupkg')78 browser = self.getBrowser(productseries, '+ubuntupkg')
72 browser.getControl('Source Package Name').value = (79 browser.getControl('Source Package Name').value = (
73 sp.sourcepackagename.name)80 sp.sourcepackagename.name)
74 browser.getControl(ubuntu_series.displayname).selected = True81 browser.getControl(ubuntu_series.displayname).selected = True
@@ -84,7 +91,7 @@
84 product = self.factory.makeProduct(91 product = self.factory.makeProduct(
85 information_type=InformationType.PROPRIETARY)92 information_type=InformationType.PROPRIETARY)
86 series = self.factory.makeProductSeries(product=product)93 series = self.factory.makeProductSeries(product=product)
87 browser = self.getViewBrowser(series)94 browser = self.getBrowser(series)
88 tag = soupmatchers.Tag(95 tag = soupmatchers.Tag(
89 'portlet-packages', True, attrs={'id': 'portlet-packages'})96 'portlet-packages', True, attrs={'id': 'portlet-packages'})
90 self.assertThat(browser.contents, Not(soupmatchers.HTMLContains(tag)))97 self.assertThat(browser.contents, Not(soupmatchers.HTMLContains(tag)))
9198
=== modified file 'lib/lp/registry/browser/tests/test_sourcepackage_views.py'
--- lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-11 20:33:03 +0000
+++ lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-15 15:39:22 +0000
@@ -297,12 +297,16 @@
297297
298 def test_error_on_proprietary_product(self):298 def test_error_on_proprietary_product(self):
299 """Packaging cannot be created for PROPRIETARY products"""299 """Packaging cannot be created for PROPRIETARY products"""
300 product_owner = self.factory.makePerson()
301 product_name = 'proprietary-product'
300 product = self.factory.makeProduct(302 product = self.factory.makeProduct(
303 name=product_name, owner=product_owner,
301 information_type=InformationType.PROPRIETARY)304 information_type=InformationType.PROPRIETARY)
302 ubuntu_series = self.factory.makeUbuntuDistroSeries()305 ubuntu_series = self.factory.makeUbuntuDistroSeries()
303 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)306 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
304 browser = self.getViewBrowser(sp, '+edit-packaging')307 browser = self.getViewBrowser(
305 browser.getControl('Project').value = product.name308 sp, '+edit-packaging', user=product_owner)
309 browser.getControl('Project').value = product_name
306 browser.getControl('Continue').click()310 browser.getControl('Continue').click()
307 self.assertIn(311 self.assertIn(
308 'Only Public projects can be packaged, not Proprietary.',312 'Only Public projects can be packaged, not Proprietary.',
@@ -310,14 +314,18 @@
310314
311 def test_error_on_proprietary_productseries(self):315 def test_error_on_proprietary_productseries(self):
312 """Packaging cannot be created for PROPRIETARY productseries"""316 """Packaging cannot be created for PROPRIETARY productseries"""
313 product = self.factory.makeProduct()317 product_owner = self.factory.makePerson()
318 product_name = 'proprietary-product'
319 product = self.factory.makeProduct(
320 name=product_name, owner=product_owner)
314 series = self.factory.makeProductSeries(product=product)321 series = self.factory.makeProductSeries(product=product)
315 ubuntu_series = self.factory.makeUbuntuDistroSeries()322 ubuntu_series = self.factory.makeUbuntuDistroSeries()
316 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)323 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
317 browser = self.getViewBrowser(sp, '+edit-packaging')324 browser = self.getViewBrowser(
318 browser.getControl('Project').value = product.name325 sp, '+edit-packaging', user=product_owner)
326 browser.getControl('Project').value = product_name
319 browser.getControl('Continue').click()327 browser.getControl('Continue').click()
320 with person_logged_in(product.owner):328 with person_logged_in(product_owner):
321 product.information_type = InformationType.PROPRIETARY329 product.information_type = InformationType.PROPRIETARY
322 browser.getControl(series.displayname).selected = True330 browser.getControl(series.displayname).selected = True
323 browser.getControl('Change').click()331 browser.getControl('Change').click()
324332
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/configure.zcml 2012-10-15 15:39:22 +0000
@@ -1229,11 +1229,19 @@
1229 class="lp.registry.model.product.Product">1229 class="lp.registry.model.product.Product">
1230 <allow1230 <allow
1231 interface="lp.registry.interfaces.product.IProductPublic"/>1231 interface="lp.registry.interfaces.product.IProductPublic"/>
1232 <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
1233 <allow1232 <allow
1233 interface="lp.registry.interfaces.pillar.IPillar"/>
1234 <require
1235 permission="launchpad.View"
1236 interface="lp.registry.interfaces.product.IProductLimitedView"/>
1237 <require
1238 permission="launchpad.View"
1239 interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
1240 <require
1241 permission="launchpad.View"
1234 interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>1242 interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
1235 <require1243 <require
1236 permission="launchpad.View"1244 permission="launchpad.AnyAllowedPerson"
1237 set_attributes="date_next_suggest_packaging"/>1245 set_attributes="date_next_suggest_packaging"/>
1238 <require1246 <require
1239 permission="launchpad.Driver"1247 permission="launchpad.Driver"
@@ -1316,7 +1324,7 @@
1316 translationpermission1324 translationpermission
1317 translations_usage"/>1325 translations_usage"/>
1318 <require1326 <require
1319 permission="zope.Public"1327 permission="launchpad.View"
1320 attributes="1328 attributes="
1321 qualifies_for_free_hosting"/>1329 qualifies_for_free_hosting"/>
1322 <require1330 <require
@@ -1331,7 +1339,8 @@
13311339
1332 <!-- IHasAliases -->1340 <!-- IHasAliases -->
13331341
1334 <allow1342 <require
1343 permission="launchpad.View"
1335 attributes="1344 attributes="
1336 aliases"/>1345 aliases"/>
1337 <require1346 <require
@@ -1341,14 +1350,17 @@
13411350
1342 <!-- IQuestionTarget -->1351 <!-- IQuestionTarget -->
13431352
1344 <allow interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>1353 <require
1345 <require1354 permission="launchpad.View"
1346 permission="launchpad.AnyPerson"1355 interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
1356 <require
1357 permission="launchpad.AnyAllowedPerson"
1347 interface="lp.answers.interfaces.questiontarget.IQuestionTargetView"/>1358 interface="lp.answers.interfaces.questiontarget.IQuestionTargetView"/>
13481359
1349 <!-- IFAQTarget -->1360 <!-- IFAQTarget -->
13501361
1351 <allow1362 <require
1363 permission="launchpad.View"
1352 interface="lp.answers.interfaces.faqcollection.IFAQCollection"1364 interface="lp.answers.interfaces.faqcollection.IFAQCollection"
1353 attributes="1365 attributes="
1354 findSimilarFAQs"/>1366 findSimilarFAQs"/>
@@ -1359,15 +1371,17 @@
13591371
1360 <!-- IStructuralSubscriptionTarget -->1372 <!-- IStructuralSubscriptionTarget -->
13611373
1362 <allow1374 <require
1375 permission="launchpad.View"
1363 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />1376 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
1364 <require1377 <require
1365 permission="launchpad.AnyPerson"1378 permission="launchpad.AnyAllowedPerson"
1366 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />1379 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
13671380
1368 <!-- IHasBugSupervisor -->1381 <!-- IHasBugSupervisor -->
13691382
1370 <allow1383 <require
1384 permission="launchpad.View"
1371 attributes="1385 attributes="
1372 bug_supervisor"/>1386 bug_supervisor"/>
1373 <require1387 <require
13741388
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-10-11 07:45:33 +0000
+++ lib/lp/registry/interfaces/product.py 2012-10-15 15:39:22 +0000
@@ -420,18 +420,24 @@
420 "Not applicable to 'Other/Proprietary'.")))420 "Not applicable to 'Other/Proprietary'.")))
421421
422422
423class IProductPublic(423class IProductPublic(Interface):
424
425 id = Int(title=_('The Project ID'))
426
427 def userCanView(user):
428 """True if the given user has access to this product."""
429
430
431class IProductLimitedView(
424 IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,432 IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
425 IHasDrivers, IHasExternalBugTracker, IHasIcon, IHasLogo,433 IHasDrivers, IHasExternalBugTracker, IHasIcon,
426 IHasMergeProposals, IHasMilestones, IHasExpirableBugs, IHasMugshot,434 IHasLogo, IHasMergeProposals, IHasMilestones, IHasExpirableBugs,
427 IHasOwner, IHasSprints, IHasTranslationImports, ITranslationPolicy,435 IHasMugshot, IHasOwner, IHasSprints, IHasTranslationImports,
428 IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,436 ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
429 IOfficialBugTagTargetPublic, IHasOOPSReferences, IPillar,437 IOfficialBugTagTargetPublic, IHasOOPSReferences,
430 ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):438 ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
431 """Public IProduct properties."""439 """Public IProduct properties."""
432440
433 id = Int(title=_('The Project ID'))
434
435 project = exported(441 project = exported(
436 ReferenceChoice(442 ReferenceChoice(
437 title=_('Part of'),443 title=_('Part of'),
@@ -868,9 +874,9 @@
868class IProductEditRestricted(IOfficialBugTagTargetRestricted):874class IProductEditRestricted(IOfficialBugTagTargetRestricted):
869 """`IProduct` properties which require launchpad.Edit permission."""875 """`IProduct` properties which require launchpad.Edit permission."""
870876
871 @mutator_for(IProductPublic['bug_sharing_policy'])877 @mutator_for(IProductLimitedView['bug_sharing_policy'])
872 @operation_parameters(bug_sharing_policy=copy_field(878 @operation_parameters(bug_sharing_policy=copy_field(
873 IProductPublic['bug_sharing_policy']))879 IProductLimitedView['bug_sharing_policy']))
874 @export_write_operation()880 @export_write_operation()
875 @operation_for_version("devel")881 @operation_for_version("devel")
876 def setBugSharingPolicy(bug_sharing_policy):882 def setBugSharingPolicy(bug_sharing_policy):
@@ -879,10 +885,10 @@
879 Checks authorization and entitlement.885 Checks authorization and entitlement.
880 """886 """
881887
882 @mutator_for(IProductPublic['branch_sharing_policy'])888 @mutator_for(IProductLimitedView['branch_sharing_policy'])
883 @operation_parameters(889 @operation_parameters(
884 branch_sharing_policy=copy_field(890 branch_sharing_policy=copy_field(
885 IProductPublic['branch_sharing_policy']))891 IProductLimitedView['branch_sharing_policy']))
886 @export_write_operation()892 @export_write_operation()
887 @operation_for_version("devel")893 @operation_for_version("devel")
888 def setBranchSharingPolicy(branch_sharing_policy):894 def setBranchSharingPolicy(branch_sharing_policy):
@@ -891,10 +897,10 @@
891 Checks authorization and entitlement.897 Checks authorization and entitlement.
892 """898 """
893899
894 @mutator_for(IProductPublic['specification_sharing_policy'])900 @mutator_for(IProductLimitedView['specification_sharing_policy'])
895 @operation_parameters(901 @operation_parameters(
896 specification_sharing_policy=copy_field(902 specification_sharing_policy=copy_field(
897 IProductPublic['specification_sharing_policy']))903 IProductLimitedView['specification_sharing_policy']))
898 @export_write_operation()904 @export_write_operation()
899 @operation_for_version("devel")905 @operation_for_version("devel")
900 def setSpecificationSharingPolicy(specification_sharing_policy):906 def setSpecificationSharingPolicy(specification_sharing_policy):
@@ -907,8 +913,8 @@
907class IProduct(913class IProduct(
908 IHasBugSupervisor, IProductEditRestricted,914 IHasBugSupervisor, IProductEditRestricted,
909 IProductModerateRestricted, IProductDriverRestricted,915 IProductModerateRestricted, IProductDriverRestricted,
910 IProductPublic, IQuestionTarget, IRootContext,916 IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
911 IStructuralSubscriptionTarget, IInformationType):917 IStructuralSubscriptionTarget, IInformationType, IPillar):
912 """A Product.918 """A Product.
913919
914 The Launchpad Registry describes the open source world as ProjectGroups920 The Launchpad Registry describes the open source world as ProjectGroups
915921
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-12 18:23:18 +0000
+++ lib/lp/registry/model/product.py 2012-10-15 15:39:22 +0000
@@ -69,6 +69,7 @@
69 InformationType,69 InformationType,
70 PRIVATE_INFORMATION_TYPES,70 PRIVATE_INFORMATION_TYPES,
71 PROPRIETARY_INFORMATION_TYPES,71 PROPRIETARY_INFORMATION_TYPES,
72 PUBLIC_INFORMATION_TYPES,
72 PUBLIC_PROPRIETARY_INFORMATION_TYPES,73 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
73 service_uses_launchpad,74 service_uses_launchpad,
74 ServiceUsage,75 ServiceUsage,
@@ -155,6 +156,7 @@
155 LicenseStatus,156 LicenseStatus,
156 )157 )
157from lp.registry.interfaces.productrelease import IProductReleaseSet158from lp.registry.interfaces.productrelease import IProductReleaseSet
159from lp.registry.interfaces.role import IPersonRoles
158from lp.registry.model.announcement import MakesAnnouncements160from lp.registry.model.announcement import MakesAnnouncements
159from lp.registry.model.commercialsubscription import CommercialSubscription161from lp.registry.model.commercialsubscription import CommercialSubscription
160from lp.registry.model.distribution import Distribution162from lp.registry.model.distribution import Distribution
@@ -1519,6 +1521,39 @@
15191521
1520 return weight_function1522 return weight_function
15211523
1524 @cachedproperty
1525 def _known_viewers(self):
1526 """A set of known persons able to view this product."""
1527 return set()
1528
1529 def userCanView(self, user):
1530 """See `IProductPublic`."""
1531 if self.information_type in PUBLIC_INFORMATION_TYPES:
1532 return True
1533 if user is None:
1534 return False
1535 if user.id in self._known_viewers:
1536 return True
1537 # We need the plain Storm Person object for the SQL query below
1538 # but an IPersonRoles object for the team membership checks.
1539 if IPersonRoles.providedBy(user):
1540 plain_user = user.person
1541 else:
1542 plain_user = user
1543 user = IPersonRoles(user)
1544 if (user.in_commercial_admin or user.in_admin or
1545 user.in_registry_experts):
1546 self._known_viewers.add(user.id)
1547 return True
1548 policy = getUtility(IAccessPolicySource).find(
1549 [(self, self.information_type)]).one()
1550 grants_for_user = getUtility(IAccessPolicyGrantSource).find(
1551 [(policy, plain_user)])
1552 if grants_for_user.is_empty():
1553 return False
1554 self._known_viewers.add(user.id)
1555 return True
1556
15221557
1523def get_precached_products(products, need_licences=False, need_projects=False,1558def get_precached_products(products, need_licences=False, need_projects=False,
1524 need_series=False, need_releases=False,1559 need_series=False, need_releases=False,
15251560
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-10-11 04:14:37 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-10-15 15:39:22 +0000
@@ -1837,12 +1837,14 @@
1837 api_method, api_version='devel', **kwargs).jsonBody()1837 api_method, api_version='devel', **kwargs).jsonBody()
18381838
1839 def _getPillarGranteeData(self):1839 def _getPillarGranteeData(self):
1840 pillar_uri = canonical_url(self.pillar, force_local_path=True)1840 pillar_uri = canonical_url(
1841 removeSecurityProxy(self.pillar), force_local_path=True)
1841 return self._named_get(1842 return self._named_get(
1842 'getPillarGranteeData', pillar=pillar_uri)1843 'getPillarGranteeData', pillar=pillar_uri)
18431844
1844 def _sharePillarInformation(self, pillar):1845 def _sharePillarInformation(self, pillar):
1845 pillar_uri = canonical_url(pillar, force_local_path=True)1846 pillar_uri = canonical_url(
1847 removeSecurityProxy(pillar), force_local_path=True)
1846 return self._named_post(1848 return self._named_post(
1847 'sharePillarInformation', pillar=pillar_uri,1849 'sharePillarInformation', pillar=pillar_uri,
1848 grantee=self.grantee_uri,1850 grantee=self.grantee_uri,
18491851
=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
--- lib/lp/registry/tests/test_pillaraffiliation.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/tests/test_pillaraffiliation.py 2012-10-15 15:39:22 +0000
@@ -150,7 +150,7 @@
150 Store.of(product).invalidate()150 Store.of(product).invalidate()
151 with StormStatementRecorder() as recorder:151 with StormStatementRecorder() as recorder:
152 IHasAffiliation(product).getAffiliationBadges([person])152 IHasAffiliation(product).getAffiliationBadges([person])
153 self.assertThat(recorder, HasQueryCount(Equals(5)))153 self.assertThat(recorder, HasQueryCount(Equals(4)))
154154
155 def test_distro_affiliation_query_count(self):155 def test_distro_affiliation_query_count(self):
156 # Only 2 business queries are expected, selects from:156 # Only 2 business queries are expected, selects from:
157157
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-11 14:05:29 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-15 15:39:22 +0000
@@ -15,6 +15,10 @@
15import transaction15import transaction
16from zope.component import getUtility16from zope.component import getUtility
17from zope.lifecycleevent.interfaces import IObjectModifiedEvent17from zope.lifecycleevent.interfaces import IObjectModifiedEvent
18from zope.security.checker import (
19 CheckerPublic,
20 getChecker,
21 )
18from zope.security.interfaces import Unauthorized22from zope.security.interfaces import Unauthorized
19from zope.security.proxy import removeSecurityProxy23from zope.security.proxy import removeSecurityProxy
2024
@@ -44,6 +48,7 @@
44 BugSharingPolicy,48 BugSharingPolicy,
45 EXCLUSIVE_TEAM_POLICY,49 EXCLUSIVE_TEAM_POLICY,
46 INCLUSIVE_TEAM_POLICY,50 INCLUSIVE_TEAM_POLICY,
51 SharingPermission,
47 SpecificationSharingPolicy,52 SpecificationSharingPolicy,
48 )53 )
49from lp.registry.errors import (54from lp.registry.errors import (
@@ -56,11 +61,13 @@
56 IAccessPolicySource,61 IAccessPolicySource,
57 )62 )
58from lp.registry.interfaces.oopsreferences import IHasOOPSReferences63from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
64from lp.registry.interfaces.person import IPersonSet
59from lp.registry.interfaces.product import (65from lp.registry.interfaces.product import (
60 IProduct,66 IProduct,
61 IProductSet,67 IProductSet,
62 License,68 License,
63 )69 )
70from lp.registry.interfaces.role import IPersonRoles
64from lp.registry.interfaces.series import SeriesStatus71from lp.registry.interfaces.series import SeriesStatus
65from lp.registry.model.product import (72from lp.registry.model.product import (
66 Product,73 Product,
@@ -72,6 +79,7 @@
72 celebrity_logged_in,79 celebrity_logged_in,
73 login,80 login,
74 person_logged_in,81 person_logged_in,
82 StormStatementRecorder,
75 TestCase,83 TestCase,
76 TestCaseWithFactory,84 TestCaseWithFactory,
77 WebServiceTestCase,85 WebServiceTestCase,
@@ -387,7 +395,7 @@
387 license=License.OTHER_PROPRIETARY)395 license=License.OTHER_PROPRIETARY)
388 self.assertEqual(InformationType.EMBARGOED, product.information_type)396 self.assertEqual(InformationType.EMBARGOED, product.information_type)
389 # Owner can set information_type397 # Owner can set information_type
390 with person_logged_in(product.owner):398 with person_logged_in(removeSecurityProxy(product).owner):
391 product.information_type = InformationType.PROPRIETARY399 product.information_type = InformationType.PROPRIETARY
392 self.assertEqual(InformationType.PROPRIETARY, product.information_type)400 self.assertEqual(InformationType.PROPRIETARY, product.information_type)
393 # Database persists information_type value401 # Database persists information_type value
@@ -399,7 +407,9 @@
399407
400 def test_product_information_type_default(self):408 def test_product_information_type_default(self):
401 # Default information_type is PUBLIC409 # Default information_type is PUBLIC
402 product = self.createProduct()410 owner = self.factory.makePerson()
411 product = getUtility(IProductSet).createProduct(
412 owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2')
403 self.assertEqual(InformationType.PUBLIC, product.information_type)413 self.assertEqual(InformationType.PUBLIC, product.information_type)
404414
405 invalid_information_types = [info_type for info_type in415 invalid_information_types = [info_type for info_type in
@@ -525,6 +535,349 @@
525 CannotChangeInformationType, 'Some series are packaged.'):535 CannotChangeInformationType, 'Some series are packaged.'):
526 product.information_type = InformationType.PROPRIETARY536 product.information_type = InformationType.PROPRIETARY
527537
538 def check_permissions(self, expected_permissions, used_permissions,
539 type_):
540 expected = set(expected_permissions.keys())
541 self.assertEqual(
542 expected, set(used_permissions.values()),
543 'Unexpected %s permissions' % type_)
544 for permission in expected_permissions:
545 attribute_names = set(
546 name for name, value in used_permissions.items()
547 if value == permission)
548 self.assertEqual(
549 expected_permissions[permission], attribute_names,
550 'Unexpected set of attributes with %s permission %s:\n'
551 'Defined but not expected: %s\n'
552 'Expected but not defined: %s'
553 % (
554 type_, permission,
555 sorted(
556 attribute_names - expected_permissions[permission]),
557 sorted(
558 expected_permissions[permission] - attribute_names)))
559
560 expected_get_permissions = {
561 CheckerPublic: set((
562 'active', 'id', 'information_type', 'pillar_category', 'private',
563 'userCanView',)),
564 'launchpad.View': set((
565 '_getOfficialTagClause', '_all_specifications',
566 '_valid_specifications', 'active_or_packaged_series',
567 'aliases', 'all_milestones',
568 'allowsTranslationEdits', 'allowsTranslationSuggestions',
569 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
570 'blueprints_usage', 'branch_sharing_policy',
571 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
572 'bug_sharing_policy', 'bug_subscriptions', 'bug_supervisor',
573 'bug_tracking_usage', 'bugtargetdisplayname', 'bugtargetname',
574 'bugtracker', 'canUserAlterAnswerContact',
575 'codehosting_usage',
576 'coming_sprints', 'commercial_subscription',
577 'commercial_subscription_is_due', 'createBug',
578 'createCustomLanguageCode', 'custom_language_codes',
579 'date_next_suggest_packaging', 'datecreated', 'description',
580 'development_focus', 'development_focusID',
581 'direct_answer_contacts', 'displayname', 'distrosourcepackages',
582 'downloadurl', 'driver', 'drivers', 'enable_bug_expiration',
583 'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
584 'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
585 'getAllowedBugInformationTypes',
586 'getAllowedSpecificationInformationTypes', 'getAnnouncement',
587 'getAnnouncements', 'getAnswerContactsForLanguage',
588 'getAnswerContactRecipients', 'getBranches',
589 'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
590 'getCustomLanguageCode', 'getDefaultBugInformationType',
591 'getDefaultSpecificationInformationType',
592 'getEffectiveTranslationPermission', 'getExternalBugTracker',
593 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
594 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
595 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
596 'getSeries', 'getSpecification', 'getSubscription',
597 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
598 'getTopContributors', 'getTopContributorsGroupedByCategory',
599 'getTranslationGroups', 'getTranslationImportQueueEntries',
600 'getTranslators', 'getUsedBugTagsWithOpenCounts',
601 'getVersionSortedSeries',
602 'has_current_commercial_subscription',
603 'has_custom_language_codes', 'has_milestones', 'homepage_content',
604 'homepageurl', 'icon', 'invitesTranslationEdits',
605 'invitesTranslationSuggestions',
606 'license_info', 'license_status', 'licenses', 'logo', 'milestones',
607 'mugshot', 'name', 'name_with_project', 'newCodeImport',
608 'obsolete_translatable_series', 'official_answers',
609 'official_anything', 'official_blueprints', 'official_bug_tags',
610 'official_codehosting', 'official_malone', 'owner',
611 'parent_subscription_target', 'packagedInDistros', 'packagings',
612 'past_sprints', 'personHasDriverRights', 'pillar',
613 'primary_translatable', 'private_bugs',
614 'programminglang', 'project', 'qualifies_for_free_hosting',
615 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
616 'remote_product', 'removeCustomLanguageCode',
617 'screenshotsurl',
618 'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
619 'series',
620 'sharesTranslationsWithOtherSide', 'sourceforgeproject',
621 'sourcepackages', 'specification_sharing_policy', 'specifications',
622 'sprints', 'summary', 'target_type_display', 'title',
623 'translatable_packages', 'translatable_series',
624 'translation_focus', 'translationgroup', 'translationgroups',
625 'translationpermission', 'translations_usage', 'ubuntu_packages',
626 'userCanAlterBugSubscription', 'userCanAlterSubscription',
627 'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
628 'wikiurl')),
629 'launchpad.AnyAllowedPerson': set((
630 'addAnswerContact', 'addBugSubscription',
631 'addBugSubscriptionFilter', 'addSubscription',
632 'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
633 'removeBugSubscription')),
634 'launchpad.Append': set(('newFAQ', )),
635 'launchpad.Driver': set(('newSeries', )),
636 'launchpad.Edit': set((
637 'addOfficialBugTag', 'removeOfficialBugTag',
638 'setBranchSharingPolicy', 'setBugSharingPolicy',
639 'setSpecificationSharingPolicy')),
640 'launchpad.Moderate': set((
641 'is_permitted', 'license_approved', 'project_reviewed',
642 'reviewer_whiteboard', 'setAliases')),
643 }
644
645 def test_get_permissions(self):
646 product = self.factory.makeProduct()
647 checker = getChecker(product)
648 self.check_permissions(
649 self.expected_get_permissions, checker.get_permissions, 'get')
650
651 def test_set_permissions(self):
652 expected_set_permissions = {
653 'launchpad.BugSupervisor': set((
654 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
655 'bugtracker', 'enable_bug_expiration',
656 'enable_bugfiling_duplicate_search', 'official_bug_tags',
657 'official_malone', 'remote_product')),
658 'launchpad.Edit': set((
659 'answers_usage', 'blueprints_usage', 'bug_supervisor',
660 'bug_tracking_usage', 'codehosting_usage',
661 'commercial_subscription', 'description', 'development_focus',
662 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
663 'homepage_content', 'homepageurl', 'icon', 'information_type',
664 'license_info', 'licenses', 'logo', 'mugshot',
665 'official_answers', 'official_blueprints',
666 'official_codehosting', 'owner', 'private',
667 'programminglang', 'project', 'redeemSubscriptionVoucher',
668 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
669 'summary', 'title', 'uses_launchpad', 'wikiurl')),
670 'launchpad.Moderate': set((
671 'active', 'autoupdate', 'license_approved', 'name',
672 'project_reviewed', 'registrant', 'reviewer_whiteboard')),
673 'launchpad.TranslationsAdmin': set((
674 'translation_focus', 'translationgroup',
675 'translationpermission', 'translations_usage')),
676 'launchpad.AnyAllowedPerson': set((
677 'date_next_suggest_packaging', )),
678 }
679 product = self.factory.makeProduct()
680 checker = getChecker(product)
681 self.check_permissions(
682 expected_set_permissions, checker.set_permissions, 'set')
683
684 def test_access_launchpad_View_public_product(self):
685 # Everybody, including anonymous users, has access to
686 # properties of public products that require the permission
687 # launchpad.View
688 product = self.factory.makeProduct()
689 names = self.expected_get_permissions['launchpad.View']
690 with person_logged_in(None):
691 for attribute_name in names:
692 getattr(product, attribute_name)
693 ordinary_user = self.factory.makePerson()
694 with person_logged_in(ordinary_user):
695 for attribute_name in names:
696 getattr(product, attribute_name)
697 with person_logged_in(product.owner):
698 for attribute_name in names:
699 getattr(product, attribute_name)
700
701 def test_access_launchpad_View_public_inactive_product(self):
702 # Everybody, including anonymous users, has access to
703 # properties of public but inactvie products that require
704 # the permission launchpad.View.
705 product = self.factory.makeProduct()
706 removeSecurityProxy(product).active = False
707 names = self.expected_get_permissions['launchpad.View']
708 with person_logged_in(None):
709 for attribute_name in names:
710 getattr(product, attribute_name)
711 ordinary_user = self.factory.makePerson()
712 with person_logged_in(ordinary_user):
713 for attribute_name in names:
714 getattr(product, attribute_name)
715 with person_logged_in(product.owner):
716 for attribute_name in names:
717 getattr(product, attribute_name)
718
719 def test_access_launchpad_View_proprietary_product(self):
720 # Only people with grants for a private product can access
721 # attributes protected by the permission launchpad.View.
722 product = self.createProduct(
723 information_type=InformationType.PROPRIETARY,
724 license=License.OTHER_PROPRIETARY)
725 owner = removeSecurityProxy(product).owner
726 names = self.expected_get_permissions['launchpad.View']
727 with person_logged_in(None):
728 for attribute_name in names:
729 self.assertRaises(
730 Unauthorized, getattr, product, attribute_name)
731 ordinary_user = self.factory.makePerson()
732 with person_logged_in(ordinary_user):
733 for attribute_name in names:
734 self.assertRaises(
735 Unauthorized, getattr, product, attribute_name)
736 with person_logged_in(owner):
737 for attribute_name in names:
738 getattr(product, attribute_name)
739 # A user with a policy grant for the product can access attributes
740 # of a private product.
741 with person_logged_in(owner):
742 getUtility(IService, 'sharing').sharePillarInformation(
743 product, ordinary_user, owner,
744 {InformationType.PROPRIETARY: SharingPermission.ALL})
745 with person_logged_in(ordinary_user):
746 for attribute_name in names:
747 getattr(product, attribute_name)
748 # Admins can access proprietary products.
749 with celebrity_logged_in('admin'):
750 for attribute_name in names:
751 getattr(product, attribute_name)
752 with celebrity_logged_in('registry_experts'):
753 for attribute_name in names:
754 getattr(product, attribute_name)
755 # Commercial admins have access to all products.
756 with celebrity_logged_in('commercial_admin'):
757 for attribute_name in names:
758 getattr(product, attribute_name)
759
760 def test_access_launchpad_AnyAllowedPerson_public_product(self):
761 # Only logged in persons have access to properties of public products
762 # that require the permission launchpad.AnyAllowedPerson.
763 product = self.factory.makeProduct()
764 names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
765 with person_logged_in(None):
766 for attribute_name in names:
767 self.assertRaises(
768 Unauthorized, getattr, product, attribute_name)
769 ordinary_user = self.factory.makePerson()
770 with person_logged_in(ordinary_user):
771 for attribute_name in names:
772 getattr(product, attribute_name)
773 with person_logged_in(product.owner):
774 for attribute_name in names:
775 getattr(product, attribute_name)
776
777 def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
778 # Only people with grants for a private product can access
779 # attributes protected by the permission launchpad.AnyAllowedPerson.
780 product = self.createProduct(
781 information_type=InformationType.PROPRIETARY,
782 license=License.OTHER_PROPRIETARY)
783 owner = removeSecurityProxy(product).owner
784 names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
785 with person_logged_in(None):
786 for attribute_name in names:
787 self.assertRaises(
788 Unauthorized, getattr, product, attribute_name)
789 ordinary_user = self.factory.makePerson()
790 with person_logged_in(ordinary_user):
791 for attribute_name in names:
792 self.assertRaises(
793 Unauthorized, getattr, product, attribute_name)
794 with person_logged_in(owner):
795 for attribute_name in names:
796 getattr(product, attribute_name)
797 # A user with a policy grant for the product can access attributes
798 # of a private product.
799 with person_logged_in(owner):
800 getUtility(IService, 'sharing').sharePillarInformation(
801 product, ordinary_user, owner,
802 {InformationType.PROPRIETARY: SharingPermission.ALL})
803 with person_logged_in(ordinary_user):
804 for attribute_name in names:
805 getattr(product, attribute_name)
806
807 def test_set_launchpad_AnyAllowedPerson_public_product(self):
808 # Only logged in users can set attributes protected by the
809 # permission launchpad.AnyAllowedPerson.
810 product = self.factory.makeProduct()
811 with person_logged_in(None):
812 self.assertRaises(
813 Unauthorized, setattr, product, 'date_next_suggest_packaging',
814 'foo')
815 ordinary_user = self.factory.makePerson()
816 with person_logged_in(ordinary_user):
817 setattr(product, 'date_next_suggest_packaging', 'foo')
818 with person_logged_in(product.owner):
819 setattr(product, 'date_next_suggest_packaging', 'foo')
820
821 def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
822 # Only people with grants for a private product can set
823 # attributes protected by the permission launchpad.AnyAllowedPerson.
824 product = self.createProduct(
825 information_type=InformationType.PROPRIETARY,
826 license=License.OTHER_PROPRIETARY)
827 owner = removeSecurityProxy(product).owner
828 with person_logged_in(None):
829 self.assertRaises(
830 Unauthorized, setattr, product, 'date_next_suggest_packaging',
831 'foo')
832 ordinary_user = self.factory.makePerson()
833 with person_logged_in(ordinary_user):
834 self.assertRaises(
835 Unauthorized, setattr, product, 'date_next_suggest_packaging',
836 'foo')
837 with person_logged_in(owner):
838 setattr(product, 'date_next_suggest_packaging', 'foo')
839 # A user with a policy grant for the product can access attributes
840 # of a private product.
841 with person_logged_in(owner):
842 getUtility(IService, 'sharing').sharePillarInformation(
843 product, ordinary_user, owner,
844 {InformationType.PROPRIETARY: SharingPermission.ALL})
845 with person_logged_in(ordinary_user):
846 setattr(product, 'date_next_suggest_packaging', 'foo')
847
848 def test_userCanView_caches_known_users(self):
849 # userCanView() maintains a cache of users known to have the
850 # permission to access a product.
851 product = self.createProduct(
852 information_type=InformationType.PROPRIETARY,
853 license=License.OTHER_PROPRIETARY)
854 owner = removeSecurityProxy(product).owner
855 user = self.factory.makePerson()
856 with person_logged_in(owner):
857 getUtility(IService, 'sharing').sharePillarInformation(
858 product, user, owner,
859 {InformationType.PROPRIETARY: SharingPermission.ALL})
860 with person_logged_in(user):
861 with StormStatementRecorder() as recorder:
862 # The first access to a property of the product from
863 # a user requires a DB query.
864 product.homepageurl
865 queries_for_first_user_access = len(recorder.queries)
866 # The second access does not require another query.
867 product.description
868 self.assertEqual(
869 queries_for_first_user_access, len(recorder.queries))
870
871 def test_userCanView_works_with_IPersonRoles(self):
872 # userCanView() maintains a cache of users known to have the
873 # permission to access a product.
874 product = self.createProduct(
875 information_type=InformationType.PROPRIETARY,
876 license=License.OTHER_PROPRIETARY)
877 user = self.factory.makePerson()
878 product.userCanView(user)
879 product.userCanView(IPersonRoles(user))
880
528881
529class TestProductBugInformationTypes(TestCaseWithFactory):882class TestProductBugInformationTypes(TestCaseWithFactory):
530883
531884
=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
--- lib/lp/registry/tests/test_product_webservice.py 2012-10-11 04:18:37 +0000
+++ lib/lp/registry/tests/test_product_webservice.py 2012-10-15 15:39:22 +0000
@@ -17,6 +17,7 @@
17from lp.services.webapp.publisher import canonical_url17from lp.services.webapp.publisher import canonical_url
18from lp.testing import TestCaseWithFactory18from lp.testing import TestCaseWithFactory
19from lp.testing.layers import DatabaseFunctionalLayer19from lp.testing.layers import DatabaseFunctionalLayer
20from lp.testing import person_logged_in
20from lp.testing.pages import (21from lp.testing.pages import (
21 LaunchpadWebServiceCaller,22 LaunchpadWebServiceCaller,
22 webservice_for_person,23 webservice_for_person,
@@ -47,27 +48,30 @@
47 layer = DatabaseFunctionalLayer48 layer = DatabaseFunctionalLayer
4849
49 def patch(self, webservice, obj, **data):50 def patch(self, webservice, obj, **data):
51 with person_logged_in(webservice.user):
52 path = URI(canonical_url(obj)).path
50 return webservice.patch(53 return webservice.patch(
51 URI(canonical_url(obj)).path,54 path, 'application/json', json.dumps(data), api_version='devel')
52 'application/json', json.dumps(data),
53 api_version='devel')
5455
55 def test_branch_sharing_policy_can_be_set(self):56 def test_branch_sharing_policy_can_be_set(self):
56 # branch_sharing_policy can be set via the API.57 # branch_sharing_policy can be set via the API.
57 product = self.factory.makeProduct()58 product = self.factory.makeProduct()
59 owner = product.owner
58 self.factory.makeCommercialSubscription(product=product)60 self.factory.makeCommercialSubscription(product=product)
59 webservice = webservice_for_person(61 webservice = webservice_for_person(
60 product.owner, permission=OAuthPermission.WRITE_PRIVATE)62 owner, permission=OAuthPermission.WRITE_PRIVATE)
61 response = self.patch(63 response = self.patch(
62 webservice, product, branch_sharing_policy='Proprietary')64 webservice, product, branch_sharing_policy='Proprietary')
63 self.assertEqual(209, response.status)65 self.assertEqual(209, response.status)
64 self.assertEqual(66 with person_logged_in(owner):
65 BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)67 self.assertEqual(
68 BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
6669
67 def test_branch_sharing_policy_non_commercial(self):70 def test_branch_sharing_policy_non_commercial(self):
68 # An API attempt to set a commercial-only branch_sharing_policy71 # An API attempt to set a commercial-only branch_sharing_policy
69 # on a non-commercial project returns Forbidden.72 # on a non-commercial project returns Forbidden.
70 product = self.factory.makeProduct()73 product = self.factory.makeProduct()
74 owner = product.owner
71 webservice = webservice_for_person(75 webservice = webservice_for_person(
72 product.owner, permission=OAuthPermission.WRITE_PRIVATE)76 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
73 response = self.patch(77 response = self.patch(
@@ -76,25 +80,29 @@
76 status=403,80 status=403,
77 body=('A current commercial subscription is required to use '81 body=('A current commercial subscription is required to use '
78 'proprietary branches.')))82 'proprietary branches.')))
79 self.assertEqual(83 with person_logged_in(owner):
80 BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)84 self.assertEqual(
85 BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)
8186
82 def test_bug_sharing_policy_can_be_set(self):87 def test_bug_sharing_policy_can_be_set(self):
83 # bug_sharing_policy can be set via the API.88 # bug_sharing_policy can be set via the API.
84 product = self.factory.makeProduct()89 product = self.factory.makeProduct()
90 owner = product.owner
85 self.factory.makeCommercialSubscription(product=product)91 self.factory.makeCommercialSubscription(product=product)
86 webservice = webservice_for_person(92 webservice = webservice_for_person(
87 product.owner, permission=OAuthPermission.WRITE_PRIVATE)93 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
88 response = self.patch(94 response = self.patch(
89 webservice, product, bug_sharing_policy='Proprietary')95 webservice, product, bug_sharing_policy='Proprietary')
90 self.assertEqual(209, response.status)96 self.assertEqual(209, response.status)
91 self.assertEqual(97 with person_logged_in(owner):
92 BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)98 self.assertEqual(
99 BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
93100
94 def test_bug_sharing_policy_non_commercial(self):101 def test_bug_sharing_policy_non_commercial(self):
95 # An API attempt to set a commercial-only bug_sharing_policy102 # An API attempt to set a commercial-only bug_sharing_policy
96 # on a non-commercial project returns Forbidden.103 # on a non-commercial project returns Forbidden.
97 product = self.factory.makeProduct()104 product = self.factory.makeProduct()
105 owner = product.owner
98 webservice = webservice_for_person(106 webservice = webservice_for_person(
99 product.owner, permission=OAuthPermission.WRITE_PRIVATE)107 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
100 response = self.patch(108 response = self.patch(
@@ -103,13 +111,14 @@
103 status=403,111 status=403,
104 body=('A current commercial subscription is required to use '112 body=('A current commercial subscription is required to use '
105 'proprietary bugs.')))113 'proprietary bugs.')))
106 self.assertEqual(114 with person_logged_in(owner):
107 BugSharingPolicy.PUBLIC, product.bug_sharing_policy)115 self.assertEqual(
116 BugSharingPolicy.PUBLIC, product.bug_sharing_policy)
108117
109 def fetch_product(self, webservice, product, api_version):118 def fetch_product(self, webservice, product, api_version):
110 return webservice.get(119 with person_logged_in(webservice.user):
111 canonical_url(product, force_local_path=True),120 url = canonical_url(product, force_local_path=True)
112 api_version=api_version).jsonBody()121 return webservice.get(url, api_version=api_version).jsonBody()
113122
114 def test_security_contact_exported(self):123 def test_security_contact_exported(self):
115 # security_contact is exported for 1.0, but not for other versions.124 # security_contact is exported for 1.0, but not for other versions.
116125
=== modified file 'lib/lp/registry/tests/test_subscribers.py'
--- lib/lp/registry/tests/test_subscribers.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/tests/test_subscribers.py 2012-10-15 15:39:22 +0000
@@ -195,7 +195,10 @@
195 # If there is no request, there is no reason to show a message in195 # If there is no request, there is no reason to show a message in
196 # the browser.196 # the browser.
197 product, user = self.make_product_user([License.GNU_GPL_V2])197 product, user = self.make_product_user([License.GNU_GPL_V2])
198 notification = LicenseNotification(product)198 # Using the proxied product leads to an exeception when
199 # notification.display() below is called because the permission
200 # checks product require an interaction.
201 notification = LicenseNotification(removeSecurityProxy(product))
199 logout()202 logout()
200 result = notification.display()203 result = notification.display()
201 self.assertIs(False, result)204 self.assertIs(False, result)
202205
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-10-09 08:39:54 +0000
+++ lib/lp/security.py 2012-10-15 15:39:22 +0000
@@ -29,6 +29,7 @@
29from lp.answers.interfaces.questionmessage import IQuestionMessage29from lp.answers.interfaces.questionmessage import IQuestionMessage
30from lp.answers.interfaces.questionsperson import IQuestionsPerson30from lp.answers.interfaces.questionsperson import IQuestionsPerson
31from lp.answers.interfaces.questiontarget import IQuestionTarget31from lp.answers.interfaces.questiontarget import IQuestionTarget
32from lp.app.enums import PUBLIC_INFORMATION_TYPES
32from lp.app.interfaces.launchpad import ILaunchpadCelebrities33from lp.app.interfaces.launchpad import ILaunchpadCelebrities
33from lp.app.interfaces.security import IAuthorization34from lp.app.interfaces.security import IAuthorization
34from lp.app.security import (35from lp.app.security import (
@@ -424,6 +425,30 @@
424 return user.isOwner(self.obj) or user.in_admin425 return user.isOwner(self.obj) or user.in_admin
425426
426427
428class ViewProduct(AuthorizationBase):
429 permission = 'launchpad.View'
430 usedfor = IProduct
431
432 def checkAuthenticated(self, user):
433 return self.obj.userCanView(user)
434
435 def checkUnauthenticated(self):
436 return self.obj.information_type in PUBLIC_INFORMATION_TYPES
437
438
439class ChangeProduct(ViewProduct):
440 """Used for attributes of IProduct that are accessible to any logged
441 in user for public product but only to persons with access grants
442 for private products.
443 """
444
445 permission = 'launchpad.AnyAllowedPerson'
446 usedfor = IProduct
447
448 def checkUnauthenticated(self):
449 return False
450
451
427class EditProduct(EditByOwnersOrAdmins):452class EditProduct(EditByOwnersOrAdmins):
428 usedfor = IProduct453 usedfor = IProduct
429454
430455
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-10-11 04:21:07 +0000
+++ lib/lp/testing/factory.py 2012-10-15 15:39:22 +0000
@@ -1025,7 +1025,6 @@
1025 specification_sharing_policy)1025 specification_sharing_policy)
1026 if information_type is not None:1026 if information_type is not None:
1027 naked_product.information_type = information_type1027 naked_product.information_type = information_type
1028
1029 return product1028 return product
10301029
1031 def makeProductSeries(self, product=None, name=None, owner=None,1030 def makeProductSeries(self, product=None, name=None, owner=None,
@@ -1044,7 +1043,7 @@
1044 if product is None:1043 if product is None:
1045 product = self.makeProduct()1044 product = self.makeProduct()
1046 if owner is None:1045 if owner is None:
1047 owner = product.owner1046 owner = removeSecurityProxy(product).owner
1048 if name is None:1047 if name is None:
1049 name = self.getUniqueString()1048 name = self.getUniqueString()
1050 if summary is None:1049 if summary is None:
@@ -1821,7 +1820,8 @@
18211820
1822 if owner is None:1821 if owner is None:
1823 owner = self.makePerson()1822 owner = self.makePerson()
1824 return removeSecurityProxy(bug).addTask(owner, target)1823 return removeSecurityProxy(bug).addTask(
1824 owner, removeSecurityProxy(target))
18251825
1826 def makeBugNomination(self, bug=None, target=None):1826 def makeBugNomination(self, bug=None, target=None):
1827 """Create and return a BugNomination.1827 """Create and return a BugNomination.
18281828
=== modified file 'lib/lp/testing/pages.py'
--- lib/lp/testing/pages.py 2012-10-08 10:07:11 +0000
+++ lib/lp/testing/pages.py 2012-10-15 15:39:22 +0000
@@ -730,7 +730,9 @@
730 request_token.review(person, permission, context)730 request_token.review(person, permission, context)
731 access_token = request_token.createAccessToken()731 access_token = request_token.createAccessToken()
732 logout()732 logout()
733 return LaunchpadWebServiceCaller(consumer_key, access_token.key)733 service = LaunchpadWebServiceCaller(consumer_key, access_token.key)
734 service.user = person
735 return service
734736
735737
736def setupDTCBrowser():738def setupDTCBrowser():
737739
=== modified file 'lib/lp/translations/browser/tests/test_noindex.py'
--- lib/lp/translations/browser/tests/test_noindex.py 2012-10-08 10:07:11 +0000
+++ lib/lp/translations/browser/tests/test_noindex.py 2012-10-15 15:39:22 +0000
@@ -46,7 +46,12 @@
46 # Using create_initialized_view for distroseries causes an error when46 # Using create_initialized_view for distroseries causes an error when
47 # rendering the view due to the way the view is registered and menus47 # rendering the view due to the way the view is registered and menus
48 # are adapted. Getting the contents via a browser does work.48 # are adapted. Getting the contents via a browser does work.
49 self.user_browser.open(self.url)49 #
50 # Retrieve the URL before the user_browser is created. Products
51 # can only be access with an active interaction, and getUserBrowser()
52 # closes the current interaction.
53 url = self.url
54 self.user_browser.open(url)
50 return self.user_browser.contents55 return self.user_browser.contents
5156
52 def getRobotsDirective(self):57 def getRobotsDirective(self):
5358
=== modified file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
--- lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-10-15 15:39:22 +0000
@@ -15,6 +15,7 @@
15 >>> queue = TranslationImportQueue()15 >>> queue = TranslationImportQueue()
16 >>> product = factory.makeProduct(16 >>> product = factory.makeProduct(
17 ... translations_usage=ServiceUsage.LAUNCHPAD)17 ... translations_usage=ServiceUsage.LAUNCHPAD)
18 >>> product_displayname = product.displayname
18 >>> trunk = product.getSeries('trunk')19 >>> trunk = product.getSeries('trunk')
19 >>> uploader = factory.makePerson()20 >>> uploader = factory.makePerson()
20 >>> entry = queue.addOrUpdateEntry(21 >>> entry = queue.addOrUpdateEntry(
@@ -34,7 +35,7 @@
3435
35The details include the project the entry is for, and who uploaded it.36The details include the project the entry is for, and who uploaded it.
3637
37 >>> product.displayname in details_text38 >>> product_displayname in details_text
38 True39 True
3940
40 # Must remove the security proxy because IPerson.displayname is protected.41 # Must remove the security proxy because IPerson.displayname is protected.
4142
=== modified file 'lib/lp/translations/stories/webservice/xx-potemplate.txt'
--- lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-08 10:07:11 +0000
+++ lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-15 15:39:22 +0000
@@ -71,13 +71,14 @@
7171
72 >>> login('admin@canonical.com')72 >>> login('admin@canonical.com')
73 >>> productseries = factory.makeProductSeries()73 >>> productseries = factory.makeProductSeries()
74 >>> product_name = productseries.product.name
74 >>> potemplate_1 = factory.makePOTemplate(productseries=productseries)75 >>> potemplate_1 = factory.makePOTemplate(productseries=productseries)
75 >>> potemplate_2 = factory.makePOTemplate(productseries=productseries)76 >>> potemplate_2 = factory.makePOTemplate(productseries=productseries)
76 >>> potemplate_count = 277 >>> potemplate_count = 2
77 >>> logout()78 >>> logout()
78 >>> all_translation_templates = anon_webservice.named_get(79 >>> all_translation_templates = anon_webservice.named_get(
79 ... '/%s/%s' % (80 ... '/%s/%s' % (
80 ... productseries.product.name,81 ... product_name,
81 ... productseries.name),82 ... productseries.name),
82 ... 'getTranslationTemplates'83 ... 'getTranslationTemplates'
83 ... ).jsonBody()84 ... ).jsonBody()