Merge lp:~rharding/launchpad/related_projects_1063272 into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 16185
Proposed branch: lp:~rharding/launchpad/related_projects_1063272
Merge into: lp:launchpad
Diff against target: 520 lines (+269/-33)
8 files modified
lib/lp/registry/browser/person.py (+10/-5)
lib/lp/registry/doc/person-account.txt (+3/-3)
lib/lp/registry/interfaces/person.py (+18/-2)
lib/lp/registry/interfaces/product.py (+7/-0)
lib/lp/registry/model/person.py (+109/-18)
lib/lp/registry/model/product.py (+9/-0)
lib/lp/registry/tests/test_person.py (+97/-5)
lib/lp/registry/tests/test_product.py (+16/-0)
To merge this branch: bzr merge lp:~rharding/launchpad/related_projects_1063272
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+130414@code.launchpad.net

Commit message

Filter affiliatedPillars based on access grants to hide non-visible products.

Description of the change

= Summary =

The person getAffiliatedPillars doesn't take into account products that are
not visible to the user. This adds the same checks as
product.getProductPrivacyFilter in order to clean that data.

== Pre Implementation ==

Talked with Orange about if this could/should be converted to storm or
performed with raw sql adjustments. The storm/union/sorting issues kept it as
raw sql at this time.

Talked with Curtis and Deryck around handling the account deactivation issue
this ran smack into.

== Implementation Notes ==

Due to the nature of the query here, with unions that must be in a specific
sorted order, the product method could not be used. It's logic was turned into
raw sql and used to filter out the product part of the union.

This makes sure that commercial admins and admins maintain access/visibility
by passing in the current viewing user to the underlying methods in order to
check for permissions.

_genAffiliatedProductSql is created to contain the logic about how the product
part of the union is to be done. It uses the old query if you're an
admin/commercial admin, however it filters based on granted products
otherwise. See product.getPrivacyFilter for the logic that was copied to
construct this query.

As a side effect, we ran into the fact that account deactivation uses
getAffiliatedPillars, which we've updated.

The decision was made that accounts that are owners of non-public products
cannot be deactivated. This needed to be added to view validation. In order to
prevent duplicate code, the validation was added to the person model. It
checks for a list of non-public products owned by the user. For validation
purposes, we needed two methods. One that's just a boolean T/F, the second
however, is needed to get readable error message back to the view to be placed
in front of the user. Because of this we've added both canDeactivateUser and
canDeactivateUserWithErrors.

The validation checking in Person required a new query on Product so that we
can easily and quickly get the list of non-public products. We needed to do a
full query to get the list so we can provide the names of those products to
the user in the validation error message.

The final trick was to add a new can_deactivate kwarg so that we can prevent
the query being hit twice, once in the view and once in the model. Since the
view did the check it can let the model know it's been sanity checked already.

There's a drive by change requested by Curtis. There's no sense setting the bug supervisor or the driver to registry experts. They are nullable values. So we set them to null and only change the owner to help with registry spam.

== QA ==

Log in as a user and create a private product. Then visit the user's
+related-projects page and it should load just fine.

Log out and as an anonymous user also load the same url without issue. No
private products should be visible to the anonymous user.

Deactivating the user with the private product should not be permitted. It
must be re-assigned to another user before deactivation is permitted.

== Tests ==

registry/tests/test_person.py
registry/tests/test_product.py

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Rick,

Some comments on your branch:

* In genAffiliatedProductSql there is a lot of repetition in the queries that could be factored out so you DRY as the first query you return is a the same as the final one minus one clause. Give a shot at refactoring if you don't mind.

* Thanks for the nice error messages when the account cannot be deactivated.

* The tests are clear and easy to follow. Thanks.

review: Approve (code)
Revision history for this message
Richard Harding (rharding) wrote :

> Hi Rick,
>
> Some comments on your branch:
>
> * In genAffiliatedProductSql there is a lot of repetition in the queries that
> could be factored out so you DRY as the first query you return is a the same
> as the final one minus one clause. Give a shot at refactoring if you don't
> mind.

Thanks for the catch. Was a series of iterations over that and didn't get back to cleaning it up. I've reused the base_query for the second build now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2012-10-22 02:30:44 +0000
+++ lib/lp/registry/browser/person.py 2012-10-24 09:24:22 +0000
@@ -635,7 +635,8 @@
635 def projects(self):635 def projects(self):
636 target = '+related-projects'636 target = '+related-projects'
637 text = 'Related projects'637 text = 'Related projects'
638 enabled = bool(self.person.getAffiliatedPillars())638 user = getUtility(ILaunchBag).user
639 enabled = bool(self.person.getAffiliatedPillars(user))
639 return Link(target, text, enabled=enabled, icon='info')640 return Link(target, text, enabled=enabled, icon='info')
640641
641 def subscriptions(self):642 def subscriptions(self):
@@ -936,12 +937,15 @@
936937
937 def validate(self, data):938 def validate(self, data):
938 """See `LaunchpadFormView`."""939 """See `LaunchpadFormView`."""
939 if self.context.account_status != AccountStatus.ACTIVE:940 can_deactivate, errors = self.context.canDeactivateAccountWithErrors()
940 self.addError('This account is already deactivated.')941 if not can_deactivate:
942 [self.addError(message) for message in errors]
941943
942 @action(_("Deactivate My Account"), name="deactivate")944 @action(_("Deactivate My Account"), name="deactivate")
943 def deactivate_action(self, action, data):945 def deactivate_action(self, action, data):
944 self.context.deactivateAccount(data['comment'])946 # We override the can_deactivate since validation already processed
947 # this information.
948 self.context.deactivateAccount(data['comment'], can_deactivate=True)
945 logoutPerson(self.request)949 logoutPerson(self.request)
946 self.request.response.addInfoNotification(950 self.request.response.addInfoNotification(
947 _(u'Your account has been deactivated.'))951 _(u'Your account has been deactivated.'))
@@ -3517,7 +3521,8 @@
3517 @cachedproperty3521 @cachedproperty
3518 def _related_projects(self):3522 def _related_projects(self):
3519 """Return all projects owned or driven by this person."""3523 """Return all projects owned or driven by this person."""
3520 return self.context.getAffiliatedPillars()3524 user = getUtility(ILaunchBag).user
3525 return self.context.getAffiliatedPillars(user)
35213526
3522 def _tableHeaderMessage(self, count, label='package'):3527 def _tableHeaderMessage(self, count, label='package'):
3523 """Format a header message for the tables on the summary page."""3528 """Format a header message for the tables on the summary page."""
35243529
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2012-09-28 14:35:35 +0000
+++ lib/lp/registry/doc/person-account.txt 2012-10-24 09:24:22 +0000
@@ -104,7 +104,7 @@
104 True104 True
105105
106 >>> foobar_pillars = []106 >>> foobar_pillars = []
107 >>> for pillar_name in foobar.getAffiliatedPillars():107 >>> for pillar_name in foobar.getAffiliatedPillars(foobar):
108 ... pillar = pillar_name.pillar108 ... pillar = pillar_name.pillar
109 ... if pillar.owner == foobar or pillar.driver == foobar:109 ... if pillar.owner == foobar or pillar.driver == foobar:
110 ... foobar_pillars.append(pillar_name)110 ... foobar_pillars.append(pillar_name)
@@ -183,7 +183,7 @@
183183
184...no owned or driven pillars...184...no owned or driven pillars...
185185
186 >>> foobar.getAffiliatedPillars().count()186 >>> foobar.getAffiliatedPillars(foobar).count()
187 0187 0
188188
189...and, finally, to not be considered a valid person in Launchpad.189...and, finally, to not be considered a valid person in Launchpad.
@@ -198,7 +198,7 @@
198 >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities198 >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
199 >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts199 >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
200200
201 >>> registry_pillars = set(registry_experts.getAffiliatedPillars())201 >>> registry_pillars = set(registry_experts.getAffiliatedPillars(foobar))
202 >>> registry_pillars.issuperset(foobar_pillars)202 >>> registry_pillars.issuperset(foobar_pillars)
203 True203 True
204204
205205
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2012-10-16 00:48:55 +0000
+++ lib/lp/registry/interfaces/person.py 2012-10-24 09:24:22 +0000
@@ -1132,7 +1132,7 @@
1132 the icons which represent that category.1132 the icons which represent that category.
1133 """1133 """
11341134
1135 def getAffiliatedPillars():1135 def getAffiliatedPillars(user):
1136 """Return the pillars that this person directly has a role with.1136 """Return the pillars that this person directly has a role with.
11371137
1138 Returns distributions, project groups, and projects that this person1138 Returns distributions, project groups, and projects that this person
@@ -1729,7 +1729,20 @@
1729class IPersonSpecialRestricted(Interface):1729class IPersonSpecialRestricted(Interface):
1730 """IPerson methods that require launchpad.Special permission to use."""1730 """IPerson methods that require launchpad.Special permission to use."""
17311731
1732 def deactivateAccount(comment):1732 def canDeactivateAccount():
1733 """Verify we safely deactivate this user account.
1734
1735 :return: True if the person can be deactivated, False otherwise.
1736 """
1737
1738 def canDeactivateAccountWithErrors():
1739 """See canDeactivateAccount with the addition of error messages for
1740 why the account cannot be deactivated.
1741
1742 :return tuple: boolean, list of error messages.
1743 """
1744
1745 def deactivateAccount(comment, can_deactivate=None):
1733 """Deactivate this person's Launchpad account.1746 """Deactivate this person's Launchpad account.
17341747
1735 Deactivating an account means:1748 Deactivating an account means:
@@ -1740,6 +1753,9 @@
1740 - Changing the ownership of products/projects/teams owned by him.1753 - Changing the ownership of products/projects/teams owned by him.
17411754
1742 :param comment: An explanation of why the account status changed.1755 :param comment: An explanation of why the account status changed.
1756 :param can_deactivate: Override the check if we can deactivate by
1757 supplying a known value. If None, then the method will run the
1758 checks.
1743 """1759 """
17441760
1745 def reactivate(comment, preferred_email):1761 def reactivate(comment, preferred_email):
17461762
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-10-16 13:59:06 +0000
+++ lib/lp/registry/interfaces/product.py 2012-10-24 09:24:22 +0000
@@ -942,6 +942,13 @@
942 all_active = Attribute(942 all_active = Attribute(
943 "All the active products, sorted newest first.")943 "All the active products, sorted newest first.")
944944
945 def get_users_private_products(user):
946 """Get users non-public products.
947
948 :param user: Which user are we searching products for.
949 :return: An iterable of IProduct
950 """
951
945 def get_all_active(eager_load=True):952 def get_all_active(eager_load=True):
946 """Get all active products.953 """Get all active products.
947954
948955
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-10-22 02:30:44 +0000
+++ lib/lp/registry/model/person.py 2012-10-24 09:24:22 +0000
@@ -109,7 +109,10 @@
109109
110from lp import _110from lp import _
111from lp.answers.model.questionsperson import QuestionsPersonMixin111from lp.answers.model.questionsperson import QuestionsPersonMixin
112from lp.app.enums import PRIVATE_INFORMATION_TYPES112from lp.app.enums import (
113 InformationType,
114 PRIVATE_INFORMATION_TYPES,
115 )
113from lp.app.interfaces.launchpad import (116from lp.app.interfaces.launchpad import (
114 IHasIcon,117 IHasIcon,
115 IHasLogo,118 IHasLogo,
@@ -202,7 +205,10 @@
202from lp.registry.interfaces.personnotification import IPersonNotificationSet205from lp.registry.interfaces.personnotification import IPersonNotificationSet
203from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource206from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
204from lp.registry.interfaces.pillar import IPillarNameSet207from lp.registry.interfaces.pillar import IPillarNameSet
205from lp.registry.interfaces.product import IProduct208from lp.registry.interfaces.product import (
209 IProduct,
210 IProductSet,
211 )
206from lp.registry.interfaces.projectgroup import IProjectGroup212from lp.registry.interfaces.projectgroup import IProjectGroup
207from lp.registry.interfaces.role import IPersonRoles213from lp.registry.interfaces.role import IPersonRoles
208from lp.registry.interfaces.ssh import (214from lp.registry.interfaces.ssh import (
@@ -1030,19 +1036,64 @@
1030 cur.execute(query)1036 cur.execute(query)
1031 return cur.fetchall()1037 return cur.fetchall()
10321038
1033 def getAffiliatedPillars(self):1039 def _genAffiliatedProductSql(self, user=None):
1040 """Helper to generate the product sql for getAffiliatePillars"""
1041 base_query = """
1042 SELECT name, 3 as kind, displayname
1043 FROM product p
1044 WHERE
1045 p.active = True
1046 AND (
1047 p.driver = %(person)s
1048 OR p.owner = %(person)s
1049 OR p.bug_supervisor = %(person)s
1050 )
1051 """ % sqlvalues(person=self)
1052
1053 if user is not None:
1054 roles = IPersonRoles(user)
1055 if roles.in_admin or roles.in_commercial_admin:
1056 return base_query
1057
1058 # This is the raw sql version of model/product getProductPrivacyFilter
1059 granted_products = """
1060 SELECT p.id
1061 FROM product p,
1062 accesspolicygrantflat apflat,
1063 teamparticipation part,
1064 accesspolicy ap
1065 WHERE
1066 apflat.grantee = part.team
1067 AND part.person = %(user)s
1068 AND apflat.policy = ap.id
1069 AND ap.product = p.id
1070 AND ap.type = p.information_type
1071 """ % sqlvalues(user=user)
1072
1073 # We have to generate the sqlvalues first so that they're properly
1074 # setup and escaped. Then we combine the above query which is already
1075 # processed.
1076 query_values = sqlvalues(information_type=InformationType.PUBLIC)
1077 query_values.update(granted_sql=granted_products)
1078
1079 query = base_query + """
1080 AND (
1081 p.information_type = %(information_type)s
1082 OR p.information_type is NULL
1083 OR p.id IN (%(granted_sql)s)
1084 )
1085 """ % query_values
1086 return query
1087
1088 def getAffiliatedPillars(self, user):
1034 """See `IPerson`."""1089 """See `IPerson`."""
1035 find_spec = (PillarName, SQL('kind'), SQL('displayname'))1090 find_spec = (PillarName, SQL('kind'), SQL('displayname'))
1036 origin = SQL("""1091 base = """PillarName
1037 PillarName1092 JOIN (
1038 JOIN (1093 %s
1039 SELECT name, 3 as kind, displayname1094 """ % self._genAffiliatedProductSql(user=user)
1040 FROM product1095
1041 WHERE1096 origin = base + """
1042 active = True AND
1043 (driver = %(person)s
1044 OR owner = %(person)s
1045 OR bug_supervisor = %(person)s)
1046 UNION1097 UNION
1047 SELECT name, 2 as kind, displayname1098 SELECT name, 2 as kind, displayname
1048 FROM project1099 FROM project
@@ -1059,8 +1110,9 @@
1059 OR bug_supervisor = %(person)s1110 OR bug_supervisor = %(person)s
1060 ) _pillar1111 ) _pillar
1061 ON PillarName.name = _pillar.name1112 ON PillarName.name = _pillar.name
1062 """ % sqlvalues(person=self))1113 """ % sqlvalues(person=self)
1063 results = IStore(self).using(origin).find(find_spec)1114
1115 results = IStore(self).using(SQL(origin)).find(find_spec)
1064 results = results.order_by('kind', 'displayname')1116 results = results.order_by('kind', 'displayname')
10651117
1066 def get_pillar_name(result):1118 def get_pillar_name(result):
@@ -2109,15 +2161,47 @@
2109 clauseTables=['Person'],2161 clauseTables=['Person'],
2110 orderBy=Person.sortingColumns)2162 orderBy=Person.sortingColumns)
21112163
2164 def canDeactivateAccount(self):
2165 """See `IPerson`."""
2166 can_deactivate, errors = self.canDeactivateAccountWithErrors()
2167 return can_deactivate
2168
2169 def canDeactivateAccountWithErrors(self):
2170 """See `IPerson`."""
2171 # Users that own non-public products cannot be deactivated until the
2172 # products are reassigned.
2173 errors = []
2174 product_set = getUtility(IProductSet)
2175 non_public_products = product_set.get_users_private_products(self)
2176 if non_public_products.count() != 0:
2177 errors.append(('This account cannot be deactivated because it owns '
2178 'the following non-public products: ') +
2179 ','.join([p.name for p in non_public_products]))
2180
2181 if self.account_status != AccountStatus.ACTIVE:
2182 errors.append('This account is already deactivated.')
2183
2184 return (not errors), errors
2185
2112 # XXX: salgado, 2009-04-16: This should be called just deactivate(),2186 # XXX: salgado, 2009-04-16: This should be called just deactivate(),
2113 # because it not only deactivates this person's account but also the2187 # because it not only deactivates this person's account but also the
2114 # person.2188 # person.
2115 def deactivateAccount(self, comment):2189 def deactivateAccount(self, comment, can_deactivate=None):
2116 """See `IPersonSpecialRestricted`."""2190 """See `IPersonSpecialRestricted`."""
2117 if not self.is_valid_person:2191 if not self.is_valid_person:
2118 raise AssertionError(2192 raise AssertionError(
2119 "You can only deactivate an account of a valid person.")2193 "You can only deactivate an account of a valid person.")
21202194
2195 if can_deactivate is None:
2196 # The person can only be deactivated if they do not own any
2197 # non-public products.
2198 can_deactivate = self.canDeactivateAccount()
2199
2200 if not can_deactivate:
2201 message = ("You cannot deactivate an account that owns a "
2202 "non-public product.")
2203 raise AssertionError(message)
2204
2121 for membership in self.team_memberships:2205 for membership in self.team_memberships:
2122 self.leave(membership.team)2206 self.leave(membership.team)
21232207
@@ -2146,7 +2230,7 @@
2146 registry_experts = getUtility(ILaunchpadCelebrities).registry_experts2230 registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
2147 for team in Person.selectBy(teamowner=self):2231 for team in Person.selectBy(teamowner=self):
2148 team.teamowner = registry_experts2232 team.teamowner = registry_experts
2149 for pillar_name in self.getAffiliatedPillars():2233 for pillar_name in self.getAffiliatedPillars(self):
2150 pillar = pillar_name.pillar2234 pillar = pillar_name.pillar
2151 # XXX flacoste 2007-11-26 bug=164635 The comparison using id below2235 # XXX flacoste 2007-11-26 bug=164635 The comparison using id below
2152 # works around a nasty intermittent failure.2236 # works around a nasty intermittent failure.
@@ -2155,9 +2239,16 @@
2155 pillar.owner = registry_experts2239 pillar.owner = registry_experts
2156 changed = True2240 changed = True
2157 if pillar.driver is not None and pillar.driver.id == self.id:2241 if pillar.driver is not None and pillar.driver.id == self.id:
2158 pillar.driver = registry_experts2242 pillar.driver = None
2159 changed = True2243 changed = True
21602244
2245 # Products need to change the bug supervisor as well.
2246 if IProduct.providedBy(pillar):
2247 if (pillar.bug_supervisor is not None and
2248 pillar.bug_supervisor.id == self.id):
2249 pillar.bug_supervisor = None
2250 changed = True
2251
2161 if not changed:2252 if not changed:
2162 # Since we removed the person from all teams, something is2253 # Since we removed the person from all teams, something is
2163 # seriously broken here.2254 # seriously broken here.
21642255
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-22 02:30:44 +0000
+++ lib/lp/registry/model/product.py 2012-10-24 09:24:22 +0000
@@ -1735,6 +1735,15 @@
1735 Product.id.is_in(Select(Product.id, granted_products)))1735 Product.id.is_in(Select(Product.id, granted_products)))
17361736
1737 @classmethod1737 @classmethod
1738 def get_users_private_products(cls, user):
1739 """List the non-public products the user owns."""
1740 result = IStore(Product).find(
1741 Product,
1742 Product._owner == user,
1743 Product._information_type.is_in(PROPRIETARY_INFORMATION_TYPES))
1744 return result
1745
1746 @classmethod
1738 def get_all_active(cls, user, eager_load=True):1747 def get_all_active(cls, user, eager_load=True):
1739 clause = cls.getProductPrivacyFilter(user)1748 clause = cls.getProductPrivacyFilter(user)
1740 result = IStore(Product).find(Product, Product.active,1749 result = IStore(Product).find(Product, Product.active,
17411750
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-10-17 15:16:09 +0000
+++ lib/lp/registry/tests/test_person.py 2012-10-24 09:24:22 +0000
@@ -336,7 +336,7 @@
336 expected_pillars = [336 expected_pillars = [
337 distribution.name, project_group.name, project.name]337 distribution.name, project_group.name, project.name]
338 received_pillars = [338 received_pillars = [
339 pillar.name for pillar in user.getAffiliatedPillars()]339 pillar.name for pillar in user.getAffiliatedPillars(user)]
340 self.assertEqual(expected_pillars, received_pillars)340 self.assertEqual(expected_pillars, received_pillars)
341341
342 def test_getAffiliatedPillars_roles(self):342 def test_getAffiliatedPillars_roles(self):
@@ -352,7 +352,7 @@
352 expected_pillars = [352 expected_pillars = [
353 driven_project.name, owned_project.name, supervised_project.name]353 driven_project.name, owned_project.name, supervised_project.name]
354 received_pillars = [354 received_pillars = [
355 pillar.name for pillar in user.getAffiliatedPillars()]355 pillar.name for pillar in user.getAffiliatedPillars(user)]
356 self.assertEqual(expected_pillars, received_pillars)356 self.assertEqual(expected_pillars, received_pillars)
357357
358 def test_getAffiliatedPillars_active_pillars(self):358 def test_getAffiliatedPillars_active_pillars(self):
@@ -364,7 +364,76 @@
364 inactive_project.active = False364 inactive_project.active = False
365 expected_pillars = [active_project.name]365 expected_pillars = [active_project.name]
366 received_pillars = [pillar.name for pillar in366 received_pillars = [pillar.name for pillar in
367 user.getAffiliatedPillars()]367 user.getAffiliatedPillars(user)]
368 self.assertEqual(expected_pillars, received_pillars)
369
370 def test_getAffiliatedPillars_minus_embargoed(self):
371 # Skip non public products if not allowed to see them.
372 owner = self.factory.makePerson()
373 user = self.factory.makePerson()
374 self.factory.makeProduct(
375 information_type=InformationType.EMBARGOED,
376 owner=owner)
377 public = self.factory.makeProduct(
378 information_type=InformationType.PUBLIC,
379 owner=owner)
380
381 expected_pillars = [public.name]
382 received_pillars = [pillar.name for pillar in
383 owner.getAffiliatedPillars(user)]
384 self.assertEqual(expected_pillars, received_pillars)
385
386 def test_getAffiliatedPillars_visible_to_self(self):
387 # Users can see their own non-public affiliated products.
388 owner = self.factory.makePerson()
389 self.factory.makeProduct(
390 name=u'embargoed',
391 information_type=InformationType.EMBARGOED,
392 owner=owner)
393 self.factory.makeProduct(
394 name=u'public',
395 information_type=InformationType.PUBLIC,
396 owner=owner)
397
398 expected_pillars = [u'embargoed', u'public']
399 received_pillars = [pillar.name for pillar in
400 owner.getAffiliatedPillars(owner)]
401 self.assertEqual(expected_pillars, received_pillars)
402
403 def test_getAffiliatedPillars_visible_to_admins(self):
404 # Users can see their own non-public affiliated products.
405 owner = self.factory.makePerson()
406 admin = self.factory.makeAdministrator()
407 self.factory.makeProduct(
408 name=u'embargoed',
409 information_type=InformationType.EMBARGOED,
410 owner=owner)
411 self.factory.makeProduct(
412 name=u'public',
413 information_type=InformationType.PUBLIC,
414 owner=owner)
415
416 expected_pillars = [u'embargoed', u'public']
417 received_pillars = [pillar.name for pillar in
418 owner.getAffiliatedPillars(admin)]
419 self.assertEqual(expected_pillars, received_pillars)
420
421 def test_getAffiliatedPillars_visible_to_commercial_admins(self):
422 # Users can see their own non-public affiliated products.
423 owner = self.factory.makePerson()
424 admin = self.factory.makeCommercialAdmin()
425 self.factory.makeProduct(
426 name=u'embargoed',
427 information_type=InformationType.EMBARGOED,
428 owner=owner)
429 self.factory.makeProduct(
430 name=u'public',
431 information_type=InformationType.PUBLIC,
432 owner=owner)
433
434 expected_pillars = [u'embargoed', u'public']
435 received_pillars = [pillar.name for pillar in
436 owner.getAffiliatedPillars(admin)]
368 self.assertEqual(expected_pillars, received_pillars)437 self.assertEqual(expected_pillars, received_pillars)
369438
370 def test_no_merge_pending(self):439 def test_no_merge_pending(self):
@@ -675,6 +744,26 @@
675 self.bzr = product_set.getByName('bzr')744 self.bzr = product_set.getByName('bzr')
676 self.now = datetime.now(pytz.UTC)745 self.now = datetime.now(pytz.UTC)
677746
747 def test_canDeactivateAccount_private_projects(self):
748 """A user owning non-public products cannot be deactivated."""
749 user = self.factory.makePerson()
750 public_product = self.factory.makeProduct(
751 information_type=InformationType.PUBLIC,
752 name="public",
753 owner=user)
754 public_product = self.factory.makeProduct(
755 information_type=InformationType.PROPRIETARY,
756 name="private",
757 owner=user)
758
759 login(user.preferredemail.email)
760 can_deactivate, errors = user.canDeactivateAccountWithErrors()
761
762 self.assertFalse(can_deactivate)
763 expected_error = ('This account cannot be deactivated because it owns '
764 'the following non-public products: private')
765 self.assertIn(expected_error, errors)
766
678 def test_deactivateAccount_copes_with_names_already_in_use(self):767 def test_deactivateAccount_copes_with_names_already_in_use(self):
679 """When a user deactivates his account, its name is changed.768 """When a user deactivates his account, its name is changed.
680769
@@ -710,12 +799,15 @@
710 product = self.factory.makeProduct(owner=user)799 product = self.factory.makeProduct(owner=user)
711 with person_logged_in(user):800 with person_logged_in(user):
712 product.driver = user801 product.driver = user
802 product.bug_supervisor = user
713 user.deactivateAccount("Going off the grid.")803 user.deactivateAccount("Going off the grid.")
714 registry_team = getUtility(ILaunchpadCelebrities).registry_experts804 registry_team = getUtility(ILaunchpadCelebrities).registry_experts
715 self.assertEqual(registry_team, product.owner,805 self.assertEqual(registry_team, product.owner,
716 "Owner is not registry team.")806 "Owner is not registry team.")
717 self.assertEqual(registry_team, product.driver,807 self.assertEqual(None, product.driver,
718 "Driver is not registry team.")808 "Driver is not emptied.")
809 self.assertEqual(None, product.bug_supervisor,
810 "Driver is not emptied.")
719811
720 def test_getDirectMemberIParticipateIn(self):812 def test_getDirectMemberIParticipateIn(self):
721 sample_person = Person.byName('name12')813 sample_person = Person.byName('name12')
722814
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-18 15:01:49 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-24 09:24:22 +0000
@@ -1866,6 +1866,22 @@
1866 clause = ProductSet.getProductPrivacyFilter(user)1866 clause = ProductSet.getProductPrivacyFilter(user)
1867 return IStore(Product).find(Product, clause)1867 return IStore(Product).find(Product, clause)
18681868
1869 def test_users_private_products(self):
1870 # Ignore any public products the user may own.
1871 owner = self.factory.makePerson()
1872 public = self.factory.makeProduct(
1873 information_type=InformationType.PUBLIC,
1874 owner=owner)
1875 proprietary = self.factory.makeProduct(
1876 information_type=InformationType.PROPRIETARY,
1877 owner=owner)
1878 embargoed = self.factory.makeProduct(
1879 information_type=InformationType.EMBARGOED,
1880 owner=owner)
1881 result = ProductSet.get_users_private_products(owner)
1882 self.assertIn(proprietary, result)
1883 self.assertIn(embargoed, result)
1884
1869 def test_get_all_active_omits_proprietary(self):1885 def test_get_all_active_omits_proprietary(self):
1870 # Ignore proprietary products for anonymous users1886 # Ignore proprietary products for anonymous users
1871 proprietary = self.factory.makeProduct(1887 proprietary = self.factory.makeProduct(