Merge lp:~sinzui/launchpad/merge-karma-1 into lp:launchpad/db-devel

Proposed by Curtis Hovey
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 9941
Proposed branch: lp:~sinzui/launchpad/merge-karma-1
Merge into: lp:launchpad/db-devel
Diff against target: 259 lines (+147/-30)
3 files modified
database/schema/security.cfg (+2/-2)
lib/lp/registry/model/person.py (+46/-3)
lib/lp/registry/tests/test_person.py (+99/-25)
To merge this branch: bzr merge lp:~sinzui/launchpad/merge-karma-1
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+39763@code.launchpad.net

Description of the change

This is my branch to fix karma left on merged accounts.

    lp:~sinzui/launchpad/merge-karma-1
    Diff size: 250
    Launchpad bug:
        https://bugs.launchpad.net/bugs/190242
        https://bugs.launchpad.net/bugs/3189
        https://bugs.launchpad.net/bugs/301750
    Test command: ./bin/test -vv -t TestPersonSetMerge
    Pre-implementation: Edwin
    Target release: 10.11

Fix karma left on merged accounts
---------------------------------

Merged profiles appear in the top contributors for a project because the
karma cache was not purged. The links are 404 errors (190242). User often
believe karma was lost by the merge because the karma total cache was not
updated by merge. User think they lost 1000's of karma points (3189). Users
are disappointed to see that the member since date is not updated when
merging an older profile with a younger one. (301750)

Rules
-----

    * Delete the KarmaCache and KarmaTotalCache for the merged profile
    * Add the karmaTotalcache from the merged profile so that the karma
      points look like they are combined. This is not accurate; the
      daily job will recalculate the KarmaTotalCache correctly with 24 hours.
    * Use the MIN/LEAST functions to choose the oldest date of the two
      profiles.

QA
--

On staging. Choose to merge an older profile with lots of karma that
also appears in a project top contributors list
    * Merge the profile.
    * Verify the merged profile does not appear in the top contributors
      list. (The preserved profile will not appear until karma is
      recalculated)
    * Verify that the preserved user has more karma points on its profile
      page.
    * Verify that the preserved user has the older date.

Lint
----

Linting changed files:
  database/schema/security.cfg
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_person.py

Test
----

Added tests for the two karma merging conditions and the oldest datecreated
condition.
    * lib/lp/registry/tests/test_person.py

Implementation
--------------

Added a method to update/delete KarmaCache and KarmaTotalCache. Added
a method to merge the oldest datecreated.
    * lib/lp/registry/model/person.py

Allow the webapp to delete or update KarmaCache and KarmaTotalCache.
    * database/schema/security.cfg

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Please change "if result" to "if result is None" on line 33. Otherwise, looks good.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

On Tue, Nov 2, 2010 at 7:17 AM, Aaron Bentley <email address hidden> wrote:
> Review: Approve
> Please change "if result" to "if result is None" on line 33.  Otherwise, looks good.

That would be a bug. The correct spelling is either
if result:
or
if result is not None and len(result) > 0:

if result:
is faster and logically equivalent - writing it long hand gains nothing.

Revision history for this message
Robert Collins (lifeless) wrote :

On Tue, Nov 2, 2010 at 8:11 AM, Robert Collins
<email address hidden> wrote:
> That would be a bug. The correct spelling is either
> if result:
> or
> if result is not None and len(result) > 0:
>
> if result:
> is faster and logically equivalent - writing it long hand gains nothing.

Blah, I didn't mean to be quite so black-and-white there.

I know we have a coding style guideline that said to avoid 'if foo:',
but there were two issues in the review:
- the sense of the test was inverted (from if foo to if is None)
- the change from __nonzero__ to a less complete condition which can
break if the list is empty.

The former one obviously matters most. The __nonzero__ angle I haven't
had the time to debate in our style guide yet, though I do feel
strongly about it - we've had numerous bugs because we *didn't use*
__nonzero__ for tests. We've also had bugs because we did :) - I think
we will benefit by removing the guideline altogether and just using
the most appropriate spelling at any one place.

-Rob

Revision history for this message
Aaron Bentley (abentley) wrote :

Sorry, omission of "not" is is a typo that didn't appear in my on-call review. However, writing it long hand is considered more explicit and is recommended by our current style: https://dev.launchpad.net/PythonStyleGuide#Truth%20conditionals a

Revision history for this message
Aaron Bentley (abentley) wrote :

IMO, "not None" is more precise and explicit, not less complete. If fetchone returns an empty list, it is violating its contract (http://www.python.org/dev/peps/pep-0249/), and that should be treated as an error, not as though there were no results.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2010-10-26 12:53:04 +0000
3+++ database/schema/security.cfg 2010-11-01 18:59:50 +0000
4@@ -175,9 +175,9 @@
5 public.hwvendorname = SELECT
6 public.incrementaldiff = SELECT, INSERT, UPDATE, DELETE
7 public.job = SELECT, INSERT, UPDATE, DELETE
8-public.karmacache = SELECT
9+public.karmacache = SELECT, DELETE
10 public.karmacategory = SELECT
11-public.karmatotalcache = SELECT
12+public.karmatotalcache = SELECT, DELETE, UPDATE
13 public.language = SELECT
14 public.languagepack = SELECT, INSERT, UPDATE
15 public.launchpadstatistic = SELECT
16
17=== modified file 'lib/lp/registry/model/person.py'
18--- lib/lp/registry/model/person.py 2010-10-29 22:14:33 +0000
19+++ lib/lp/registry/model/person.py 2010-11-01 18:59:50 +0000
20@@ -3778,7 +3778,7 @@
21 'AND team = %s'
22 % sqlvalues(to_id, team_id))
23 result = cur.fetchone()
24- if result:
25+ if result is not None:
26 current_status = result[0]
27 # Now we can safely delete from_person's membership record,
28 # because we know to_person has a membership entry for this
29@@ -3823,6 +3823,45 @@
30 'DELETE FROM TeamParticipation WHERE person = %s AND '
31 'team = %s' % sqlvalues(from_id, team_id))
32
33+ def _mergeKarmaCache(self, cur, from_id, to_id, from_karma):
34+ # Merge the karma total cache so the user does not think the karma
35+ # was lost.
36+ if from_karma > 0:
37+ cur.execute('''
38+ SELECT karma_total FROM KarmaTotalCache
39+ WHERE person = %(to_id)d
40+ ''' % vars())
41+ result = cur.fetchone()
42+ if result is not None:
43+ # Add the karma to the remaining user.
44+ karma_total = from_karma + result[0]
45+ cur.execute('''
46+ UPDATE KarmaTotalCache SET karma_total = %(karma_total)d
47+ WHERE person = %(to_id)d
48+ ''' % vars())
49+ else:
50+ # Make the existing karma belong to the remaining user.
51+ cur.execute('''
52+ UPDATE KarmaTotalCache SET person = %(to_id)d
53+ WHERE person = %(from_id)d
54+ ''' % vars())
55+ # Delete the old caches; the daily job will build them later.
56+ cur.execute('''
57+ DELETE FROM KarmaTotalCache WHERE person = %(from_id)d
58+ ''' % vars())
59+ cur.execute('''
60+ DELETE FROM KarmaCache WHERE person = %(from_id)d
61+ ''' % vars())
62+
63+ def _mergeDateCreated(self, cur, from_id, to_id):
64+ cur.execute('''
65+ UPDATE Person
66+ SET datecreated = (
67+ SELECT MIN(datecreated) FROM Person
68+ WHERE id in (%(to_id)d, %(from_id)d) LIMIT 1)
69+ WHERE id = %(to_id)d
70+ ''' % vars())
71+
72 def merge(self, from_person, to_person):
73 """See `IPersonSet`."""
74 # Sanity checks
75@@ -3861,8 +3900,6 @@
76 ('personlanguage', 'person'),
77 ('person', 'merged'),
78 ('emailaddress', 'person'),
79- ('karmacache', 'person'),
80- ('karmatotalcache', 'person'),
81 # Polls are not carried over when merging teams.
82 ('poll', 'team'),
83 # We can safely ignore the mailinglist table as there's a sanity
84@@ -3997,6 +4034,12 @@
85 self._mergeWebServiceBan(cur, from_id, to_id)
86 skip.append(('webserviceban', 'person'))
87
88+ self._mergeKarmaCache(cur, from_id, to_id, from_person.karma)
89+ skip.append(('karmacache', 'person'))
90+ skip.append(('karmatotalcache', 'person'))
91+
92+ self._mergeDateCreated(cur, from_id, to_id)
93+
94 # Sanity check. If we have a reference that participates in a
95 # UNIQUE index, it must have already been handled by this point.
96 # We can tell this by looking at the skip list.
97
98=== modified file 'lib/lp/registry/tests/test_person.py'
99--- lib/lp/registry/tests/test_person.py 2010-10-28 10:18:31 +0000
100+++ lib/lp/registry/tests/test_person.py 2010-11-01 18:59:50 +0000
101@@ -55,7 +55,10 @@
102 PersonVisibility,
103 )
104 from lp.registry.interfaces.product import IProductSet
105-from lp.registry.model.karma import KarmaCategory
106+from lp.registry.model.karma import (
107+ KarmaCategory,
108+ KarmaTotalCache,
109+ )
110 from lp.registry.model.person import Person
111 from lp.registry.model.structuralsubscription import StructuralSubscription
112 from lp.services.openid.model.openididentifier import OpenIdIdentifier
113@@ -447,7 +450,46 @@
114 self.assertEqual(person1, person2)
115
116
117-class TestPersonSetMerge(TestCaseWithFactory):
118+class KarmaTestMixin:
119+ """Helper methods for setting karma."""
120+
121+ def _makeKarmaCache(self, person, product, category_name_values):
122+ """Create a KarmaCache entry with the given arguments.
123+
124+ In order to create the KarmaCache record we must switch to the DB
125+ user 'karma'. This invalidates the objects under test so they
126+ must be retrieved again.
127+ """
128+ transaction.commit()
129+ reconnect_stores('karmacacheupdater')
130+ total = 0
131+ # Insert category total for person and project.
132+ for category_name, value in category_name_values:
133+ category = KarmaCategory.byName(category_name)
134+ self.cache_manager.new(
135+ value, person.id, category.id, product_id=product.id)
136+ total += value
137+ # Insert total cache for person and project.
138+ self.cache_manager.new(
139+ total, person.id, None, product_id=product.id)
140+ transaction.commit()
141+ reconnect_stores('launchpad')
142+
143+ def _makeKarmaTotalCache(self, person, total):
144+ """Create a KarmaTotalCache entry.
145+
146+ In order to create the KarmaTotalCache record we must switch to the DB
147+ user 'karma'. This invalidates the objects under test so they
148+ must be retrieved again.
149+ """
150+ transaction.commit()
151+ reconnect_stores('karmacacheupdater')
152+ KarmaTotalCache(person=person.id, karma_total=total)
153+ transaction.commit()
154+ reconnect_stores('launchpad')
155+
156+
157+class TestPersonSetMerge(TestCaseWithFactory, KarmaTestMixin):
158 """Test cases for PersonSet merge."""
159
160 layer = DatabaseFunctionalLayer
161@@ -495,6 +537,60 @@
162 self.assertIn(duplicate_identifier, merged_identifiers)
163 self.assertIn(person_identifier, merged_identifiers)
164
165+ def test_karmacache_transferred_to_user_has_no_karma(self):
166+ # Verify that the merged user has no KarmaCache entries,
167+ # and the karma total was transfered.
168+ self.cache_manager = getUtility(IKarmaCacheManager)
169+ product = self.factory.makeProduct()
170+ duplicate = self.factory.makePerson()
171+ self._makeKarmaCache(
172+ duplicate, product, [('bugs', 10)])
173+ self._makeKarmaTotalCache(duplicate, 15)
174+ # The karma changes invalidated duplicate instance.
175+ duplicate = self.person_set.get(duplicate.id)
176+ person = self.factory.makePerson()
177+ self._do_premerge(duplicate, person)
178+ login_person(person)
179+ self.person_set.merge(duplicate, person)
180+ self.assertEqual([], duplicate.karma_category_caches)
181+ self.assertEqual(0, duplicate.karma)
182+ self.assertEqual(15, person.karma)
183+
184+ def test_karmacache_transferred_to_user_has_karma(self):
185+ # Verify that the merged user has no KarmaCache entries,
186+ # and the karma total was summed.
187+ self.cache_manager = getUtility(IKarmaCacheManager)
188+ product = self.factory.makeProduct()
189+ duplicate = self.factory.makePerson()
190+ self._makeKarmaCache(
191+ duplicate, product, [('bugs', 10)])
192+ self._makeKarmaTotalCache(duplicate, 15)
193+ person = self.factory.makePerson()
194+ self._makeKarmaCache(
195+ person, product, [('bugs', 9)])
196+ self._makeKarmaTotalCache(person, 13)
197+ # The karma changes invalidated duplicate and person instances.
198+ duplicate = self.person_set.get(duplicate.id)
199+ person = self.person_set.get(person.id)
200+ self._do_premerge(duplicate, person)
201+ login_person(person)
202+ self.person_set.merge(duplicate, person)
203+ self.assertEqual([], duplicate.karma_category_caches)
204+ self.assertEqual(0, duplicate.karma)
205+ self.assertEqual(28, person.karma)
206+
207+ def test_person_date_created_preserved(self):
208+ # Verify that the oldest datecreated is merged.
209+ person = self.factory.makePerson()
210+ duplicate = self.factory.makePerson()
211+ oldest_date = datetime(
212+ 2005, 11, 25, 0, 0, 0, 0, pytz.timezone('UTC'))
213+ removeSecurityProxy(duplicate).datecreated = oldest_date
214+ self._do_premerge(duplicate, person)
215+ login_person(person)
216+ self.person_set.merge(duplicate, person)
217+ self.assertEqual(oldest_date, person.datecreated)
218+
219
220 class TestPersonSetCreateByOpenId(TestCaseWithFactory):
221 layer = DatabaseFunctionalLayer
222@@ -860,7 +956,7 @@
223 assignee=self.user)
224
225
226-class TestPersonKarma(TestCaseWithFactory):
227+class TestPersonKarma(TestCaseWithFactory, KarmaTestMixin):
228
229 layer = DatabaseFunctionalLayer
230
231@@ -878,28 +974,6 @@
232 self._makeKarmaCache(
233 self.person, self.c_product, [('code', 100), (('bugs', 50))])
234
235- def _makeKarmaCache(self, person, product, category_name_values):
236- """Create a KarmaCache entry with the given arguments.
237-
238- In order to create the KarmaCache record we must switch to the DB
239- user 'karma'. This requires a commit and invalidates the product
240- instance.
241- """
242- transaction.commit()
243- reconnect_stores('karmacacheupdater')
244- total = 0
245- # Insert category total for person and project.
246- for category_name, value in category_name_values:
247- category = KarmaCategory.byName(category_name)
248- self.cache_manager.new(
249- value, person.id, category.id, product_id=product.id)
250- total += value
251- # Insert total cache for person and project.
252- self.cache_manager.new(
253- total, person.id, None, product_id=product.id)
254- transaction.commit()
255- reconnect_stores('launchpad')
256-
257 def test__getProjectsWithTheMostKarma_ordering(self):
258 # Verify that pillars are ordered by karma.
259 results = removeSecurityProxy(

Subscribers

People subscribed via source and target branches

to status/vote changes: