Merge lp:~abentley/launchpad/proprietary-karma into lp:launchpad

Proposed by Aaron Bentley on 2012-11-20
Status: Merged
Approved by: Aaron Bentley on 2012-11-20
Approved revision: no longer in the source branch.
Merged at revision: 16294
Proposed branch: lp:~abentley/launchpad/proprietary-karma
Merge into: lp:launchpad
Diff against target: 324 lines (+90/-80)
6 files modified
lib/lp/registry/browser/person.py (+1/-1)
lib/lp/registry/browser/tests/test_person.py (+18/-22)
lib/lp/registry/doc/person-karma.txt (+3/-2)
lib/lp/registry/interfaces/person.py (+3/-7)
lib/lp/registry/model/person.py (+32/-42)
lib/lp/registry/tests/test_person.py (+33/-6)
To merge this branch: bzr merge lp:~abentley/launchpad/proprietary-karma
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-11-20 Approve on 2012-11-20
Review via email: mp+135188@code.launchpad.net

Commit Message

Filter karma for product privacy.

Description of the Change

= Summary =
Fix bug #1078470: Cannot see user's profile page because it lists a proprietary project

== Proposed fix ==
Use standard product privacy filtering code.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
Removed iterTopProjectsContributedTo, since it was unused and wanted inactive, as well as active, products. (It probably actually didn't, but hadn't been updated because it was unused.)

Stormified the query, then changed it to use Products and Distributions directly, since that's what the calling code really wants. Then changed it to return only active products on the query side. Then added the privacy filter. This reduces the query count slightly, since it no longer needs to repeatedly call IPillarNameSet).getByName. Since now every row returned by the query can be used, there is no need to return an extra 5 rows, which will reduce the number of rows returned.

Updated tests to reflect the fact that getProjectsAndCategoriesContributedTo and _getProjectsWithTheMostKarma now take a user parameter.

test_karma_category_sort was failing because it was running as the 'karma' user, which did not have permission on 'distribution'. Using the 'karma' user was not intentional; it was a side-effect of _makeKarmaCache. Changed _makeKarmaCache to use "with dbuser('karma')", so that the test runs under the default user.

== Tests ==
bin/test -t test_karma_category_sort -t person-karma.txt -t TestPersonKarma

== Demo and Q/A ==
Hard to QA because karma cache is updated by a script and staging doesn't have updated data on ~a.rosales

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/doc/person-karma.txt
  lib/lp/registry/browser/tests/test_person.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_person.py

To post a comment you must log in.
Richard Harding (rharding) wrote :

#208

Multiple imports on the one line.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2012-11-05 02:30:49 +0000
3+++ lib/lp/registry/browser/person.py 2012-11-20 16:44:21 +0000
4@@ -1644,7 +1644,7 @@
5 def contributions(self):
6 """Cache the results of getProjectsAndCategoriesContributedTo()."""
7 return self.context.getProjectsAndCategoriesContributedTo(
8- limit=5)
9+ self.user, limit=5)
10
11 @cachedproperty
12 def contributed_categories(self):
13
14=== modified file 'lib/lp/registry/browser/tests/test_person.py'
15--- lib/lp/registry/browser/tests/test_person.py 2012-11-19 05:54:50 +0000
16+++ lib/lp/registry/browser/tests/test_person.py 2012-11-20 16:44:21 +0000
17@@ -67,7 +67,10 @@
18 StormStatementRecorder,
19 TestCaseWithFactory,
20 )
21-from lp.testing.dbuser import switch_dbuser
22+from lp.testing.dbuser import (
23+ dbuser,
24+ switch_dbuser,
25+ )
26 from lp.testing.layers import (
27 DatabaseFunctionalLayer,
28 LaunchpadFunctionalLayer,
29@@ -263,29 +266,22 @@
30 'Categories are not sorted correctly')
31
32 def _makeKarmaCache(self, person, product, category, value=10):
33- """ Create and return a KarmaCache entry with the given arguments.
34+ """Create and return a KarmaCache entry with the given arguments.
35
36- In order to create the KarmaCache record we must switch to the DB
37- user 'karma', so tests that need a different user after calling
38- this method should run switch_dbuser() themselves.
39+ A commit is implicitly triggered because the 'karma' dbuser is used.
40 """
41-
42- switch_dbuser('karma')
43-
44- cache_manager = getUtility(IKarmaCacheManager)
45- karmacache = cache_manager.new(
46- value, person.id, category.id, product_id=product.id)
47-
48- try:
49- cache_manager.updateKarmaValue(
50- value, person.id, category_id=None, product_id=product.id)
51- except NotFoundError:
52- cache_manager.new(
53- value, person.id, category_id=None, product_id=product.id)
54-
55- # We must commit here so that the change is seen in other transactions
56- # (e.g. when the callsite issues a switch_dbuser() after we return).
57- transaction.commit()
58+ with dbuser('karma'):
59+ cache_manager = getUtility(IKarmaCacheManager)
60+ karmacache = cache_manager.new(
61+ value, person.id, category.id, product_id=product.id)
62+
63+ try:
64+ cache_manager.updateKarmaValue(
65+ value, person.id, category_id=None, product_id=product.id)
66+ except NotFoundError:
67+ cache_manager.new(
68+ value, person.id, category_id=None, product_id=product.id)
69+
70 return karmacache
71
72
73
74=== modified file 'lib/lp/registry/doc/person-karma.txt'
75--- lib/lp/registry/doc/person-karma.txt 2011-12-30 06:14:56 +0000
76+++ lib/lp/registry/doc/person-karma.txt 2012-11-20 16:44:21 +0000
77@@ -23,7 +23,8 @@
78 >>> from lp.registry.interfaces.karma import IKarma
79 >>> from lp.registry.interfaces.person import IPersonSet
80 >>> from lp.registry.interfaces.product import IProductSet
81- >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
82+ >>> from lp.registry.interfaces.sourcepackagename import (
83+ ... ISourcePackageNameSet)
84 >>> salgado = getUtility(IPersonSet).getByName('salgado')
85 >>> firefox = getUtility(IProductSet).getByName('firefox')
86 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
87@@ -94,7 +95,7 @@
88 show the 5 most active projects.
89
90 >>> foobar = getUtility(IPersonSet).getByName('name16')
91- >>> for contrib in foobar.getProjectsAndCategoriesContributedTo():
92+ >>> for contrib in foobar.getProjectsAndCategoriesContributedTo(None):
93 ... categories = sorted(cat.name for cat in contrib['categories'])
94 ... print contrib['project'].title, categories
95 The Evolution Groupware Application [u'bugs', u'translations']
96
97=== modified file 'lib/lp/registry/interfaces/person.py'
98--- lib/lp/registry/interfaces/person.py 2012-11-01 03:41:36 +0000
99+++ lib/lp/registry/interfaces/person.py 2012-11-20 16:44:21 +0000
100@@ -1127,10 +1127,12 @@
101 mailing_list = Attribute(
102 _("The team's mailing list, if it has one, otherwise None."))
103
104- def getProjectsAndCategoriesContributedTo(limit=10):
105+ def getProjectsAndCategoriesContributedTo(user, limit=10):
106 """Return a list of dicts with projects and the contributions made
107 by this person on that project.
108
109+ Only entries visible to the specified user will be shown.
110+
111 The list is limited to the :limit: projects this person is most
112 active.
113
114@@ -1198,12 +1200,6 @@
115 Return no more than the number given as quantity.
116 """
117
118- def iterTopProjectsContributedTo(limit=10):
119- """Iterate over the top projects contributed to.
120-
121- Iterate no more than the given limit.
122- """
123-
124 # XXX: salgado, 2008-08-01: Unexported because this method doesn't take
125 # into account whether or not a team's memberships are private.
126 # @operation_parameters(team=copy_field(ITeamMembership['team']))
127
128=== modified file 'lib/lp/registry/model/person.py'
129--- lib/lp/registry/model/person.py 2012-11-19 05:54:50 +0000
130+++ lib/lp/registry/model/person.py 2012-11-20 16:44:21 +0000
131@@ -205,7 +205,6 @@
132 )
133 from lp.registry.interfaces.personnotification import IPersonNotificationSet
134 from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
135-from lp.registry.interfaces.pillar import IPillarNameSet
136 from lp.registry.interfaces.product import (
137 IProduct,
138 IProductSet,
139@@ -264,7 +263,6 @@
140 SQLBase,
141 sqlvalues,
142 )
143-from lp.services.features import getFeatureFlag
144 from lp.services.helpers import (
145 ensure_unicode,
146 shortlist,
147@@ -989,27 +987,21 @@
148 return getUtility(IBugTaskSet).search(
149 search_params, *args, prejoins=prejoins)
150
151- def getProjectsAndCategoriesContributedTo(self, limit=5):
152+ def getProjectsAndCategoriesContributedTo(self, user, limit=5):
153 """See `IPerson`."""
154 contributions = []
155- # Pillars names have no concept of active. Extra pillars names are
156- # requested because deactivated pillars will be filtered out.
157- extra_limit = limit + 5
158- results = self._getProjectsWithTheMostKarma(limit=extra_limit)
159- for pillar_name, karma in results:
160- pillar = getUtility(IPillarNameSet).getByName(
161- pillar_name, ignore_inactive=True)
162- if pillar is not None:
163- contributions.append(
164- {'project': pillar,
165- 'categories': self._getContributedCategories(pillar)})
166- if len(contributions) == limit:
167- break
168+ results = self._getProjectsWithTheMostKarma(user, limit=limit)
169+ for product, distro, karma in results:
170+ pillar = (product or distro)
171+ contributions.append(
172+ {'project': pillar,
173+ 'categories': self._getContributedCategories(pillar)})
174 return contributions
175
176- def _getProjectsWithTheMostKarma(self, limit=10):
177- """Return the names and karma points of this person on the
178- product/distribution with that name.
179+ def _getProjectsWithTheMostKarma(self, user, limit=10):
180+ """Return the product/distribution and karma points of this person.
181+
182+ Inactive products are ignored.
183
184 The results are ordered descending by the karma points and limited to
185 the given limit.
186@@ -1017,24 +1009,27 @@
187 # We want this person's total karma on a given context (that is,
188 # across all different categories) here; that's why we use a
189 # "KarmaCache.category IS NULL" clause here.
190- query = """
191- SELECT PillarName.name, KarmaCache.karmavalue
192- FROM KarmaCache
193- JOIN PillarName ON
194- COALESCE(KarmaCache.distribution, -1) =
195- COALESCE(PillarName.distribution, -1)
196- AND
197- COALESCE(KarmaCache.product, -1) =
198- COALESCE(PillarName.product, -1)
199- WHERE person = %(person)s
200- AND KarmaCache.category IS NULL
201- AND KarmaCache.project IS NULL
202- ORDER BY karmavalue DESC, name
203- LIMIT %(limit)s;
204- """ % sqlvalues(person=self, limit=limit)
205- cur = cursor()
206- cur.execute(query)
207- return cur.fetchall()
208+ from lp.registry.model.product import (
209+ Product,
210+ ProductSet,
211+ )
212+ from lp.registry.model.distribution import Distribution
213+ tableset = Store.of(self).using(
214+ KarmaCache, LeftJoin(Product, Product.id == KarmaCache.productID),
215+ LeftJoin(Distribution, Distribution.id ==
216+ KarmaCache.distributionID))
217+ result = tableset.find(
218+ (Product, Distribution, KarmaCache.karmavalue),
219+ KarmaCache.personID == self.id,
220+ KarmaCache.category == None,
221+ KarmaCache.project == None,
222+ Or(
223+ And(Product.id != None, Product.active == True,
224+ ProductSet.getProductPrivacyFilter(user)),
225+ Distribution.id != None))
226+ result.order_by(Desc(KarmaCache.karmavalue),
227+ Coalesce(Product.name, Distribution.name))
228+ return result[:limit]
229
230 def _genAffiliatedProductSql(self, user=None):
231 """Helper to generate the product sql for getAffiliatePillars"""
232@@ -1238,11 +1233,6 @@
233 Person.id == self.id)
234 return not person.is_empty()
235
236- def iterTopProjectsContributedTo(self, limit=10):
237- getByName = getUtility(IPillarNameSet).getByName
238- for name, ignored in self._getProjectsWithTheMostKarma(limit=limit):
239- yield getByName(name)
240-
241 def _getContributedCategories(self, pillar):
242 """Return the KarmaCategories to which this person has karma on the
243 given pillar.
244
245=== modified file 'lib/lp/registry/tests/test_person.py'
246--- lib/lp/registry/tests/test_person.py 2012-10-26 07:15:49 +0000
247+++ lib/lp/registry/tests/test_person.py 2012-11-20 16:44:21 +0000
248@@ -748,11 +748,11 @@
249 def test_canDeactivateAccount_private_projects(self):
250 """A user owning non-public products cannot be deactivated."""
251 user = self.factory.makePerson()
252- public_product = self.factory.makeProduct(
253+ self.factory.makeProduct(
254 information_type=InformationType.PUBLIC,
255 name="public",
256 owner=user)
257- public_product = self.factory.makeProduct(
258+ self.factory.makeProduct(
259 information_type=InformationType.PROPRIETARY,
260 name="private",
261 owner=user)
262@@ -1101,7 +1101,9 @@
263 def test__getProjectsWithTheMostKarma_ordering(self):
264 # Verify that pillars are ordered by karma.
265 results = removeSecurityProxy(
266- self.person)._getProjectsWithTheMostKarma()
267+ self.person)._getProjectsWithTheMostKarma(None)
268+ results = [((distro or product).name, karma)
269+ for product, distro, karma in results]
270 self.assertEqual(
271 [('cc', 150), ('bb', 50), ('aa', 10)], results)
272
273@@ -1116,7 +1118,7 @@
274 # Verify that a list of projects and contributed karma categories
275 # is returned.
276 results = removeSecurityProxy(
277- self.person).getProjectsAndCategoriesContributedTo()
278+ self.person).getProjectsAndCategoriesContributedTo(None)
279 names = [entry['project'].name for entry in results]
280 self.assertEqual(
281 ['cc', 'bb', 'aa'], names)
282@@ -1132,7 +1134,7 @@
283 a_product = getUtility(IProductSet).getByName('cc')
284 a_product.active = False
285 results = removeSecurityProxy(
286- self.person).getProjectsAndCategoriesContributedTo()
287+ self.person).getProjectsAndCategoriesContributedTo(None)
288 names = [entry['project'].name for entry in results]
289 self.assertEqual(
290 ['bb', 'aa'], names)
291@@ -1149,7 +1151,32 @@
292 self._makeKarmaCache(
293 self.person, f_product, [('bugs', 3)])
294 results = removeSecurityProxy(
295- self.person).getProjectsAndCategoriesContributedTo()
296+ self.person).getProjectsAndCategoriesContributedTo(None)
297+ names = [entry['project'].name for entry in results]
298+ self.assertEqual(
299+ ['cc', 'bb', 'aa', 'dd', 'ee'], names)
300+
301+ def test_getProjectsAndCategoriesContributedTo_privacy(self):
302+ # Verify privacy is honored.
303+ d_owner = self.factory.makePerson()
304+ d_product = self.factory.makeProduct(
305+ name='dd', information_type=InformationType.PROPRIETARY,
306+ owner=d_owner)
307+ self._makeKarmaCache(
308+ self.person, d_product, [('bugs', 5)])
309+ e_product = self.factory.makeProduct(name='ee')
310+ self._makeKarmaCache(
311+ self.person, e_product, [('bugs', 4)])
312+ f_product = self.factory.makeProduct(name='ff')
313+ self._makeKarmaCache(
314+ self.person, f_product, [('bugs', 3)])
315+ results = removeSecurityProxy(
316+ self.person).getProjectsAndCategoriesContributedTo(None)
317+ names = [entry['project'].name for entry in results]
318+ self.assertEqual(
319+ ['cc', 'bb', 'aa', 'ee', 'ff'], names)
320+ results = removeSecurityProxy(
321+ self.person).getProjectsAndCategoriesContributedTo(d_owner)
322 names = [entry['project'].name for entry in results]
323 self.assertEqual(
324 ['cc', 'bb', 'aa', 'dd', 'ee'], names)