Merge lp:~sinzui/launchpad/oil-and-pigment into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/oil-and-pigment
Merge into: lp:launchpad
Diff against target: 505 lines (+174/-64)
11 files modified
lib/lp/app/stories/launchpad-root/site-search.txt (+1/-2)
lib/lp/app/templates/base-layout-macros.pt (+2/-4)
lib/lp/app/templates/launchpad-search.pt (+7/-2)
lib/lp/blueprints/templates/person-specworkload.pt (+1/-1)
lib/lp/registry/doc/person-karma.txt (+0/-8)
lib/lp/registry/model/mailinglist.py (+3/-1)
lib/lp/registry/model/person.py (+12/-5)
lib/lp/registry/stories/team/xx-team-home.txt (+1/-1)
lib/lp/registry/stories/teammembership/xx-private-membership.txt (+1/-1)
lib/lp/registry/templates/team-portlet-membership.pt (+24/-18)
lib/lp/registry/tests/test_person.py (+122/-21)
To merge this branch: bzr merge lp:~sinzui/launchpad/oil-and-pigment
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+23952@code.launchpad.net

Description of the change

This is my branch to fix from trivial issues while my brain is stuck in
first gear.

    lp:~sinzui/launchpad/oil-and-pigment
    Diff size: 489
    Launchpad bug: https://bugs.launchpad.net/bugs/436299
                   https://bugs.launchpad.net/bugs/568336
                   https://bugs.launchpad.net/bugs/130285
                   https://bugs.launchpad.net/bugs/557544
    Test command: ./bin/test -vv lp.registry.tests.test_person
        ./bin/test -t site-search -t xx-team-home -t xx-private-membership \
            -t person-karma
    Pre-implementation: no one
    Target release: 10.04

Fix from trivial issues while my brain is stuck in first gear
--------------------------------------------------------------------

Bug #436299 [Search results give wrong "Registered by" information]
    The maintainer is listed as the registrant. This is wrong. The
    problem is that distributions do not have a registrant.

Bug #568336 [Typo on profile workload page]
    There is a double 'or'

Bug #130285 [Disregard deleted projects from Most active in]
    Deactivated projects are listed in "Most active in" in the profile page
    and the links are a 404.

Bug #557544 [Bad plural on team page ("1 active members")]
    What: "1 active members" can appear on team page. This is incorrect use
    of a plural.

Rules
-----

Bug #436299 [Search results give wrong "Registered by" information]
    Verify there is a registrant using tales before appending the clause.
    tal:define="registrant view/pillar/registrant|nothing"
    tal:condition="registrant"

Bug #568336 [Typo on profile workload page]
    Remove the second 'or'.

Bug #130285 [Disregard deleted projects from lists]
    This issue in on the cusp of *not* trivial because a good understanding
    of pillar name behaviour and karma testing is needed. The fix can be
    easily placed into an existing doc test, but that is not the correct
    location.
    * pillar names (used by karma cache) have no concept of active/inactive.
      The method must filter deactivate pillars /after/ the query to get the
      names. The callsite must request more than is needed because of the
      filtering.
    * Generate the karma for a unit test of the method *and* the private
      methods that were never directly tested.
    * Test that deactivated pillars are not included.
    * ADDENDUM: While I knew how to generate the karma, the reason why
      the karma looks like it is inserted twice was not obvious from existing
      tests. I took an extra hour learning this and adding comments for
      future developers.

Bug #557544 [Bad plural on team page ("1 active members")]
    * Use the plural-message macro to switch between member and members

QA
--

Bug #436299 [Search results give wrong "Registered by" information]
    * Visit https://edge.launchpad.net/+search?field.text=wesnoth
    * Verify that Karianne Fog Heen is listed as the registrant
    * Visit https://edge.launchpad.net/+search?field.text=ubuntu
    * Verify that no registrant is listed:
      Registered on <date>

Bug #568336 [Typo on profile workload page]
    * Visit https://blueprints.edge.launchpad.net/~humphreybc/+specworkload
    * Verify text:
      Benjamin Humphrey is expected to work on, or is its creator

Bug #130285 [Disregard deleted projects from lists]
    * Visit https://edge.launchpad.net/~simon-zoellner
    * Verify that PHProute is not in "Most active in"

Bug #557544 [Bad plural on team page ("1 active members")]
    * Visit https://launchpad.net/~halfgeek.org-ixan
    * Verify the page says 1 active member

Lint
----

Linting changed files:
  lib/lp/app/stories/launchpad-root/site-search.txt
  lib/lp/app/templates/base-layout-macros.pt
  lib/lp/app/templates/launchpad-search.pt
  lib/lp/blueprints/templates/person-specworkload.pt
  lib/lp/registry/doc/person-karma.txt
  lib/lp/registry/model/person.py
  lib/lp/registry/stories/team/xx-team-home.txt
  lib/lp/registry/stories/teammembership/xx-private-membership.txt
  lib/lp/registry/templates/team-portlet-membership.pt
  lib/lp/registry/tests/test_person.py

Test
----

    * lib/lp/app/stories/launchpad-root/site-search.txt
      * Verified that distributions do not get a "registered by" line.
    * lib/lp/registry/doc/person-karma.txt
      * Moved test from doctest to unittest.
    * lib/lp/registry/stories/team/xx-team-home.txt
      * Verified singural and plural cases.
    * lib/lp/registry/stories/teammembership/xx-private-membership.txt
      * Verified singural and plural cases.
    * lib/lp/registry/tests/test_person.py
      * Lint hated this file. I removed a lot of unused imports and vars.
      * I switched the tests to run on the faster DatabaseFunctionalLayer.
      * Added a test for Person.getProjectsAndCategoriesContributedTo()
        and its private helpers. This also tests the new rule to not include
        deactivate projects.

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

    * lib/lp/app/templates/base-layout-macros.pt
      * Discovered that the macro generated white-space that is inappropirate
        for anchors and punctuation.
    * lib/lp/app/templates/launchpad-search.pt
      * Updated the template to use registrant, not owner, and only print
        the "registered by" line if the pillar has a registrant.
    * lib/lp/blueprints/templates/person-specworkload.pt
      * Removed redundant "or".
    * lib/lp/registry/model/person.py
      * Request 5 extra pillar names and built a list of 5 active pillars.
    * lib/lp/registry/templates/team-portlet-membership.pt
      * Fixed plural rule for active, proposed, and pending membership.
        Sorry that this makes the template hard to read.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (3.9 KiB)

Hi Curtis,

overall, a nice branch. I have though a few qestions regarding the
filtering of inactive projects for the karma display, so I'm marking
the MP as "needs information".

> === modified file 'lib/lp/registry/model/person.py'
> --- lib/lp/registry/model/person.py 2010-04-19 21:16:12 +0000
> +++ lib/lp/registry/model/person.py 2010-04-22 18:05:27 +0000
> @@ -850,12 +850,16 @@
> def getProjectsAndCategoriesContributedTo(self, limit=5):
> """See `IPerson`."""
> contributions = []
> - results = self._getProjectsWithTheMostKarma(limit=limit)
> - for pillar_name, karma in results:
> + # Pillars names have no concept of active. Extra pillars names are
> + # requested because deactivated pillars will be filtered out.
> + extra_limit = limit + 5
> + results = self._getProjectsWithTheMostKarma(limit=extra_limit)
> + for pillar_name, karma in results[:limit]:

While you retrieve 5 extra items calling _getProjectsWithTheMostKarma(),
you remove these extra items here ;)

> pillar = getUtility(IPillarNameSet).getByName(pillar_name)
> - contributions.append(
> - {'project': pillar,
> - 'categories': self._getContributedCategories(pillar)})
> + if pillar.active:
> + contributions.append(
> + {'project': pillar,
> + 'categories': self._getContributedCategories(pillar)})
> return contributions

I think that should be

          return contributions[:limit]

combined with a loop over the full result set.

But: Did you consider to change the SQL query in the method
_getProjectsWithTheMostKarma() itself so that only activ projects
are returned (if necessary, using an optional method parameter
"active_only=False")?

While adding five extra items to the result set looks reasonable,
there is the (admittedly largely theoretical) situation that
the result set contains more than five inactive projects, so that
the length of the "effective" list is shorter than expected.

And I would prefer it anyway to do the filtering in
_getProjectsWithTheMostKarma(), even if it would be too cumbersome
to implement in SQL.

[...]

> === modified file 'lib/lp/registry/tests/test_person.py'
> --- lib/lp/registry/tests/test_person.py 2010-02-11 19:26:26 +0000
> +++ lib/lp/registry/tests/test_person.py 2010-04-22 18:05:27 +0000

[...]

> @@ -597,5 +601,103 @@
> assignee=self.user)
>
>
> +class TestPersonKarma(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestPersonKarma, self).setUp()
> + self.person = self.factory.makePerson()
> + a_product = self.factory.makeProduct(name='aa')
> + b_product = self.factory.makeProduct(name='bb')
> + self.c_product = self.factory.makeProduct(name='cc')
> + self.cache_manager = getUtility(IKarmaCacheManager)
> + self._makeKarmaCache(
> + self.person, a_product, [('bugs', 10)])
> + self._makeKarmaCache(
> + self.person, b_product, [('answers', 50)])
> + self._makeKarmaCache(
> + self....

Read more...

review: Needs Information (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (6.1 KiB)

On Fri, 2010-04-23 at 13:41 +0000, Abel Deuring wrote:
> Review: Needs Information code
> Hi Curtis,
>
> overall, a nice branch. I have though a few qestions regarding the
> filtering of inactive projects for the karma display, so I'm marking
> the MP as "needs information".
>
> > === modified file 'lib/lp/registry/model/person.py'
> > --- lib/lp/registry/model/person.py 2010-04-19 21:16:12 +0000
> > +++ lib/lp/registry/model/person.py 2010-04-22 18:05:27 +0000
> > @@ -850,12 +850,16 @@
> > def getProjectsAndCategoriesContributedTo(self, limit=5):
> > """See `IPerson`."""
> > contributions = []
> > - results = self._getProjectsWithTheMostKarma(limit=limit)
> > - for pillar_name, karma in results:
> > + # Pillars names have no concept of active. Extra pillars names are
> > + # requested because deactivated pillars will be filtered out.
> > + extra_limit = limit + 5
> > + results = self._getProjectsWithTheMostKarma(limit=extra_limit)
> > + for pillar_name, karma in results[:limit]:
>
> While you retrieve 5 extra items calling _getProjectsWithTheMostKarma(),
> you remove these extra items here ;)

I am incompetent. I removed the limit on the iterator. I added a guard
in the loop to break at the limit.

> > pillar = getUtility(IPillarNameSet).getByName(pillar_name)
> > - contributions.append(
> > - {'project': pillar,
> > - 'categories': self._getContributedCategories(pillar)})
> > + if pillar.active:
> > + contributions.append(
> > + {'project': pillar,
> > + 'categories': self._getContributedCategories(pillar)})
> > return contributions
>
> I think that should be
>
> return contributions[:limit]
>
> combined with a loop over the full result set.

I prefer a guard in the loop. The loop may make 5 extra SQL queries for
data that will not be used. _getContributedCategories() is a call to the
db.

> But: Did you consider to change the SQL query in the method
> _getProjectsWithTheMostKarma() itself so that only activ projects
> are returned (if necessary, using an optional method parameter
> "active_only=False")?

Yes, the query would insane because it is often nothing to join on. We
are querying pillar names, not objects. that is why our code always
filters *after* the pillar_name is used to get the pillar (product,
distribution, project group)

> While adding five extra items to the result set looks reasonable,
> there is the (admittedly largely theoretical) situation that
> the result set contains more than five inactive projects, so that
> the length of the "effective" list is shorter than expected.

Yes. This hack is the same approach taken when getting a short list of
bugs that may have private ones filtered out. The complaints from users
is about 1 deactivated project in the list. We are providing coverage
for 5 deactivated projects.

> And I would prefer it anyway to do the filtering in
> _getProjectsWithTheMostKarma(), even if it would be too cumbersome
> to implement in SQL.

I do not think that is practical. As I said above, ...

Read more...

Revision history for this message
Abel Deuring (adeuring) wrote :

OK, looks good now. (Though I would still prefer to move the filtering to _getprojectsWithMostKarma(), even if it is implemented in Python)

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/stories/launchpad-root/site-search.txt'
2--- lib/lp/app/stories/launchpad-root/site-search.txt 2010-04-16 15:06:55 +0000
3+++ lib/lp/app/stories/launchpad-root/site-search.txt 2010-04-23 15:21:38 +0000
4@@ -173,8 +173,7 @@
5 Ubuntu
6 Ubuntu is a new approach to Linux Distribution that includes regular
7 releases, and a simplified single-CD installation system.
8- Registered by Ubuntu Team
9- on 2006-10-16
10+ Registered on 2006-10-16
11
12 The user enters the number 1, and he sees a bug and a question in the
13 "Exact matches" section.
14
15=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
16--- lib/lp/app/templates/base-layout-macros.pt 2010-04-09 12:30:19 +0000
17+++ lib/lp/app/templates/base-layout-macros.pt 2010-04-23 15:21:38 +0000
18@@ -438,11 +438,9 @@
19 </tal:comment>
20 <tal:singular
21 condition="python: count == 1"
22- replace="singular" />
23- <tal:plural
24+ replace="singular" /><tal:plural
25 define="l_default string:s;
26 l_plural plural | string:$singular$l_default;"
27 condition="python: count != 1"
28- replace="l_plural" />
29-</metal:plural-msg>
30+ replace="l_plural" /></metal:plural-msg>
31 </macros>
32
33=== modified file 'lib/lp/app/templates/launchpad-search.pt'
34--- lib/lp/app/templates/launchpad-search.pt 2010-04-16 15:06:55 +0000
35+++ lib/lp/app/templates/launchpad-search.pt 2010-04-23 15:21:38 +0000
36@@ -97,8 +97,13 @@
37 Project summary
38 </div>
39 <div>
40- Registered by <a
41- tal:replace="structure view/pillar/owner/fmt:link">Foo Bar</a>
42+ Registered
43+ <tal:registrant
44+ define="registrant view/pillar/registrant|nothing"
45+ condition="registrant">
46+ by
47+ <a tal:replace="structure registrant/fmt:link">Foo Bar</a>
48+ </tal:registrant>
49 <tal:XXX condition="nothing">
50 # XXX sinzui 2008-05-27:
51 # Product and ProjectGroup still use the wrong name for
52
53=== modified file 'lib/lp/blueprints/templates/person-specworkload.pt'
54--- lib/lp/blueprints/templates/person-specworkload.pt 2009-09-15 21:24:28 +0000
55+++ lib/lp/blueprints/templates/person-specworkload.pt 2010-04-23 15:21:38 +0000
56@@ -13,7 +13,7 @@
57 <p>
58 This page lists the specifications that
59 <tal:name replace="context/displayname" /> is expected to work on, or
60- or is its creator.
61+ is its creator.
62 </p>
63
64 <tal:team_definition define="is_team context/isTeam" >
65
66=== modified file 'lib/lp/registry/doc/person-karma.txt'
67--- lib/lp/registry/doc/person-karma.txt 2010-03-15 12:32:37 +0000
68+++ lib/lp/registry/doc/person-karma.txt 2010-04-23 15:21:38 +0000
69@@ -101,14 +101,6 @@
70 Mozilla Thunderbird [u'bugs']
71 Mozilla Firefox [u'bugs']
72
73-As we can see, the results are ordered descending by the karma of the person
74-on that project.
75-
76- >>> from zope.security.proxy import removeSecurityProxy
77- >>> print removeSecurityProxy(foobar)._getProjectsWithTheMostKarma()
78- [(u'evolution', 175), (u'ubuntu', 26), (u'gnomebaker', 15),
79- (u'thunderbird', 15), (u'firefox', 8), (u'bazaar', 2)]
80-
81
82 Karma Updater
83 =============
84
85=== modified file 'lib/lp/registry/model/mailinglist.py'
86--- lib/lp/registry/model/mailinglist.py 2010-04-18 18:17:05 +0000
87+++ lib/lp/registry/model/mailinglist.py 2010-04-23 15:21:38 +0000
88@@ -294,7 +294,9 @@
89 if email.id == self.team.preferredemail.id:
90 self.team.setContactAddress(None)
91 assert email.personID == self.teamID, 'Incorrectly linked email.'
92- email.status = EmailAddressStatus.NEW
93+ # Anyone with permission to deactivate a list can also set the
94+ # email address status to NEW.
95+ removeSecurityProxy(email).status = EmailAddressStatus.NEW
96
97 def reactivate(self):
98 """See `IMailingList`."""
99
100=== modified file 'lib/lp/registry/model/person.py'
101--- lib/lp/registry/model/person.py 2010-04-19 21:16:12 +0000
102+++ lib/lp/registry/model/person.py 2010-04-23 15:21:38 +0000
103@@ -850,12 +850,19 @@
104 def getProjectsAndCategoriesContributedTo(self, limit=5):
105 """See `IPerson`."""
106 contributions = []
107- results = self._getProjectsWithTheMostKarma(limit=limit)
108+ # Pillars names have no concept of active. Extra pillars names are
109+ # requested because deactivated pillars will be filtered out.
110+ extra_limit = limit + 5
111+ results = self._getProjectsWithTheMostKarma(limit=extra_limit)
112 for pillar_name, karma in results:
113- pillar = getUtility(IPillarNameSet).getByName(pillar_name)
114- contributions.append(
115- {'project': pillar,
116- 'categories': self._getContributedCategories(pillar)})
117+ pillar = getUtility(IPillarNameSet).getByName(
118+ pillar_name, ignore_inactive=True)
119+ if pillar is not None:
120+ contributions.append(
121+ {'project': pillar,
122+ 'categories': self._getContributedCategories(pillar)})
123+ if len(contributions) == limit:
124+ break
125 return contributions
126
127 def _getProjectsWithTheMostKarma(self, limit=10):
128
129=== modified file 'lib/lp/registry/stories/team/xx-team-home.txt'
130--- lib/lp/registry/stories/team/xx-team-home.txt 2010-02-25 23:53:19 +0000
131+++ lib/lp/registry/stories/team/xx-team-home.txt 2010-04-23 15:21:38 +0000
132@@ -47,7 +47,7 @@
133
134 >>> print extract_text(
135 ... find_tag_by_id(browser.contents, 'membership-summary'))
136- 10 active members, 1 invited members, 2 proposed members...
137+ 10 active members, 1 invited member, 2 proposed members...
138
139 >>> print extract_text(
140 ... find_tag_by_id(browser.contents, 'contact-email'))
141
142=== modified file 'lib/lp/registry/stories/teammembership/xx-private-membership.txt'
143--- lib/lp/registry/stories/teammembership/xx-private-membership.txt 2010-04-19 08:11:52 +0000
144+++ lib/lp/registry/stories/teammembership/xx-private-membership.txt 2010-04-23 15:21:38 +0000
145@@ -48,7 +48,7 @@
146
147 >>> print extract_text(
148 ... find_tag_by_id(browser.contents, 'membership-summary'))
149- 2 active members, 1 proposed members...
150+ 2 active members, 1 proposed member...
151
152
153 == Team Participation ==
154
155=== modified file 'lib/lp/registry/templates/team-portlet-membership.pt'
156--- lib/lp/registry/templates/team-portlet-membership.pt 2010-02-25 23:53:19 +0000
157+++ lib/lp/registry/templates/team-portlet-membership.pt 2010-04-23 15:21:38 +0000
158@@ -16,27 +16,33 @@
159 <div id="membership-summary" class="portletBody portletContent"
160 style="margin-bottom: 1.5em;">
161 <div>
162- <div id="membership-counts">
163- <img src="/@@/team" alt="team" />
164- <strong id="approved-member-count">
165+ <div id="membership-counts"
166+ tal:define="singular string:member; plural string:members">
167+ <strong class="sprite team" id="approved-member-count">
168 <tal:active content="context/all_member_count" />
169 </strong>
170 <a tal:attributes="href string:${context/fmt:url/+members}#active"
171- >active members</a><tal:invited
172- define="invited_member_count context/invited_member_count"
173- condition="invited_member_count">,
174- <strong id="invited-member-count">
175- <tal:invited_count content="context/invited_member_count" />
176- </strong>
177- <a tal:attributes="href string:${context/fmt:url/+members}#invited"
178- >invited members</a></tal:invited><tal:proposed
179- define="proposed_member_count context/proposed_member_count"
180- condition="proposed_member_count">,
181- <strong id="proposed-member-count">
182- <tal:proposed_count content="proposed_member_count" />
183- </strong>
184- <a tal:attributes="href string:${context/fmt:url/+members}#proposed"
185- >proposed members</a>
186+ tal:define="count context/all_member_count">active
187+ <tal:plural
188+ metal:use-macro="context/@@+base-layout-macros/plural-message"
189+ /></a><tal:invited
190+ define="count context/invited_member_count" condition="count">,
191+ <strong id="invited-member-count">
192+ <tal:invited_count content="count" />
193+ </strong>
194+ <a tal:attributes="href string:${context/fmt:url/+members}#invited"
195+ >invited
196+ <tal:plural
197+ metal:use-macro="context/@@+base-layout-macros/plural-message"
198+ /></a></tal:invited><tal:proposed
199+ define="count context/proposed_member_count" condition="count">,
200+ <strong id="proposed-member-count">
201+ <tal:proposed_count content="count" />
202+ </strong>
203+ <a tal:attributes="href string:${context/fmt:url/+members}#proposed"
204+ >proposed
205+ <tal:plural
206+ metal:use-macro="context/@@+base-layout-macros/plural-message"/></a>
207 </tal:proposed>
208 </div>
209 </div>
210
211=== modified file 'lib/lp/registry/tests/test_person.py'
212--- lib/lp/registry/tests/test_person.py 2010-02-11 19:26:26 +0000
213+++ lib/lp/registry/tests/test_person.py 2010-04-23 15:21:38 +0000
214@@ -7,6 +7,8 @@
215 from datetime import datetime
216 import pytz
217
218+import transaction
219+
220 from zope.component import getUtility
221 from zope.interface import providedBy
222 from zope.security.proxy import removeSecurityProxy
223@@ -19,6 +21,7 @@
224 EmailAddressAlreadyTaken, IEmailAddressSet, InvalidEmailAddress)
225 from lazr.lifecycle.snapshot import Snapshot
226 from lp.blueprints.interfaces.specification import ISpecificationSet
227+from lp.registry.interfaces.karma import IKarmaCacheManager
228 from lp.registry.interfaces.person import InvalidName
229 from lp.registry.interfaces.product import IProductSet
230 from lp.registry.interfaces.mailinglist import IMailingListSet
231@@ -28,6 +31,7 @@
232 from canonical.launchpad.database import Bug, BugTask, BugSubscription
233 from lp.registry.model.structuralsubscription import (
234 StructuralSubscription)
235+from lp.registry.model.karma import KarmaCategory
236 from lp.registry.model.person import Person
237 from lp.bugs.model.bugtask import get_related_bugtasks_search_params
238 from lp.bugs.interfaces.bugtask import IllegalRelatedBugTasksParams
239@@ -37,8 +41,8 @@
240 from lp.testing.views import create_initialized_view
241 from lp.registry.interfaces.mailinglist import MailingListStatus
242 from lp.registry.interfaces.person import PrivatePersonLinkageError
243-from canonical.testing.layers import (
244- DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
245+from canonical.testing.layers import DatabaseFunctionalLayer, reconnect_stores
246+
247
248 class TestPerson(TestCaseWithFactory):
249
250@@ -137,13 +141,12 @@
251 setattr, specification, attr_name, self.myteam)
252
253 def test_visibility_validator_announcement(self):
254- announcement = self.bzr.announce(
255+ self.bzr.announce(
256 user = self.otherteam,
257 title = 'title foo',
258 summary = 'summary foo',
259 url = 'http://foo.com',
260- publication_date = self.now
261- )
262+ publication_date = self.now)
263 try:
264 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
265 except ImmutableVisibilityError, exc:
266@@ -166,7 +169,7 @@
267 self.assertEqual(fake_warning, warning)
268
269 def test_visibility_validator_answer_contact(self):
270- answer_contact = AnswerContact(
271+ AnswerContact(
272 person=self.otherteam,
273 product=self.bzr,
274 distribution=None,
275@@ -180,7 +183,7 @@
276 'it is referenced by an answercontact.')
277
278 def test_visibility_validator_archive(self):
279- archive = getUtility(IArchiveSet).new(
280+ getUtility(IArchiveSet).new(
281 owner=self.otherteam,
282 description='desc foo',
283 purpose=ArchivePurpose.PPA)
284@@ -193,7 +196,7 @@
285 'it is referenced by an archive.')
286
287 def test_visibility_validator_branch(self):
288- branch = self.factory.makeProductBranch(
289+ self.factory.makeProductBranch(
290 registrant=self.otherteam,
291 owner=self.otherteam,
292 product=self.bzr)
293@@ -262,7 +265,7 @@
294
295 def test_visibility_validator_team_mailinglist_public(self):
296 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
297- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
298+ getUtility(IMailingListSet).new(self.otherteam)
299 try:
300 self.otherteam.visibility = PersonVisibility.PUBLIC
301 except ImmutableVisibilityError, exc:
302@@ -272,7 +275,7 @@
303
304 def test_visibility_validator_team_mailinglist_public_view(self):
305 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
306- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
307+ getUtility(IMailingListSet).new(self.otherteam)
308 # The view should add an error notification.
309 view = create_initialized_view(self.otherteam, '+edit', {
310 'field.name': 'otherteam',
311@@ -299,7 +302,7 @@
312 self.assertEqual(self.otherteam.visibility, PersonVisibility.PUBLIC)
313
314 def test_visibility_validator_team_mailinglist_private(self):
315- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
316+ getUtility(IMailingListSet).new(self.otherteam)
317 try:
318 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
319 except ImmutableVisibilityError, exc:
320@@ -310,7 +313,7 @@
321
322 def test_visibility_validator_team_mailinglist_private_view(self):
323 # The view should add a field error.
324- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
325+ getUtility(IMailingListSet).new(self.otherteam)
326 view = create_initialized_view(self.otherteam, '+edit', {
327 'field.name': 'otherteam',
328 'field.displayname': 'Other Team',
329@@ -329,14 +332,14 @@
330 # A PRIVATE_MEMBERSHIP team with a mailing list may convert to a
331 # PRIVATE.
332 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
333- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
334+ getUtility(IMailingListSet).new(self.otherteam)
335 self.otherteam.visibility = PersonVisibility.PRIVATE
336
337 def test_visibility_validator_team_mailinglist_pmt_to_private_view(self):
338 # A PRIVATE_MEMBERSHIP team with a mailing list may convert to a
339 # PRIVATE.
340 self.otherteam.visibility = PersonVisibility.PRIVATE_MEMBERSHIP
341- mailinglist = getUtility(IMailingListSet).new(self.otherteam)
342+ getUtility(IMailingListSet).new(self.otherteam)
343 view = create_initialized_view(self.otherteam, '+edit', {
344 'field.name': 'otherteam',
345 'field.displayname': 'Other Team',
346@@ -377,7 +380,7 @@
347 # A PUBLIC team with a structural subscription to a product can
348 # convert to a PRIVATE team.
349 foo_bar = Person.byName('name16')
350- sub = StructuralSubscription(
351+ StructuralSubscription(
352 product=self.bzr, subscriber=self.otherteam,
353 subscribed_by=foo_bar)
354 self.otherteam.visibility = PersonVisibility.PRIVATE
355@@ -435,7 +438,7 @@
356
357 class TestPersonSet(unittest.TestCase):
358 """Test `IPersonSet`."""
359- layer = LaunchpadFunctionalLayer
360+ layer = DatabaseFunctionalLayer
361
362 def setUp(self):
363 login(ANONYMOUS)
364@@ -463,7 +466,7 @@
365
366 class TestCreatePersonAndEmail(unittest.TestCase):
367 """Test `IPersonSet`.createPersonAndEmail()."""
368- layer = LaunchpadFunctionalLayer
369+ layer = DatabaseFunctionalLayer
370
371 def setUp(self):
372 login(ANONYMOUS)
373@@ -499,7 +502,7 @@
374
375 class TestPersonRelatedBugTaskSearch(TestCaseWithFactory):
376
377- layer = LaunchpadFunctionalLayer
378+ layer = DatabaseFunctionalLayer
379
380 def setUp(self):
381 super(TestPersonRelatedBugTaskSearch, self).setUp()
382@@ -521,7 +524,8 @@
383 # With no specified options, get_related_bugtasks_search_params()
384 # returns 4 BugTaskSearchParams objects, each with a different
385 # user field set.
386- search_params = get_related_bugtasks_search_params(self.user, self.context)
387+ search_params = get_related_bugtasks_search_params(
388+ self.user, self.context)
389 self.assertEqual(len(search_params), 4)
390 self.checkUserFields(
391 search_params[0], assignee=self.context)
392@@ -533,8 +537,8 @@
393 search_params[3], bug_commenter=self.context)
394
395 def test_get_related_bugtasks_search_params_with_assignee(self):
396- # With assignee specified, get_related_bugtasks_search_params() returns
397- # 3 BugTaskSearchParams objects.
398+ # With assignee specified, get_related_bugtasks_search_params()
399+ # returns 3 BugTaskSearchParams objects.
400 search_params = get_related_bugtasks_search_params(
401 self.user, self.context, assignee=self.user)
402 self.assertEqual(len(search_params), 3)
403@@ -597,5 +601,102 @@
404 assignee=self.user)
405
406
407+class TestPersonKarma(TestCaseWithFactory):
408+
409+ layer = DatabaseFunctionalLayer
410+
411+ def setUp(self):
412+ super(TestPersonKarma, self).setUp()
413+ self.person = self.factory.makePerson()
414+ a_product = self.factory.makeProduct(name='aa')
415+ b_product = self.factory.makeProduct(name='bb')
416+ self.c_product = self.factory.makeProduct(name='cc')
417+ self.cache_manager = getUtility(IKarmaCacheManager)
418+ self._makeKarmaCache(
419+ self.person, a_product, [('bugs', 10)])
420+ self._makeKarmaCache(
421+ self.person, b_product, [('answers', 50)])
422+ self._makeKarmaCache(
423+ self.person, self.c_product, [('code', 100), (('bugs', 50))])
424+
425+ def _makeKarmaCache(self, person, product, category_name_values):
426+ """Create a KarmaCache entry with the given arguments.
427+
428+ In order to create the KarmaCache record we must switch to the DB
429+ user 'karma'. This requires a commit and invalidates the product
430+ instance.
431+ """
432+ transaction.commit()
433+ reconnect_stores('karmacacheupdater')
434+ total = 0
435+ # Insert category total for person and project.
436+ for category_name, value in category_name_values:
437+ category = KarmaCategory.byName(category_name)
438+ self.cache_manager.new(
439+ value, person.id, category.id, product_id=product.id)
440+ total += value
441+ # Insert total cache for person and project.
442+ self.cache_manager.new(
443+ total, person.id, None, product_id=product.id)
444+ transaction.commit()
445+ reconnect_stores('launchpad')
446+
447+ def test__getProjectsWithTheMostKarma_ordering(self):
448+ # Verify that pillars are ordered by karma.
449+ results = removeSecurityProxy(
450+ self.person)._getProjectsWithTheMostKarma()
451+ self.assertEqual(
452+ [('cc', 150), ('bb', 50), ('aa', 10)], results)
453+
454+ def test__getContributedCategories(self):
455+ # Verify that a iterable of karma categories is returned.
456+ categories = removeSecurityProxy(
457+ self.person)._getContributedCategories(self.c_product)
458+ names = sorted(category.name for category in categories)
459+ self.assertEqual(['bugs', 'code'], names)
460+
461+ def test_getProjectsAndCategoriesContributedTo(self):
462+ # Verify that a list of projects and contributed karma categories
463+ # is returned.
464+ results = removeSecurityProxy(
465+ self.person).getProjectsAndCategoriesContributedTo()
466+ names = [entry['project'].name for entry in results]
467+ self.assertEqual(
468+ ['cc', 'bb', 'aa'], names)
469+ project_categories = results[0]
470+ names = [
471+ category.name for category in project_categories['categories']]
472+ self.assertEqual(
473+ ['code', 'bugs'], names)
474+
475+ def test_getProjectsAndCategoriesContributedTo_active_only(self):
476+ # Verify that deactivated pillars are not included.
477+ login('admin@canonical.com')
478+ a_product = getUtility(IProductSet).getByName('cc')
479+ a_product.active = False
480+ results = removeSecurityProxy(
481+ self.person).getProjectsAndCategoriesContributedTo()
482+ names = [entry['project'].name for entry in results]
483+ self.assertEqual(
484+ ['bb', 'aa'], names)
485+
486+ def test_getProjectsAndCategoriesContributedTo_limit(self):
487+ # Verify the limit of 5 is honored.
488+ d_product = self.factory.makeProduct(name='dd')
489+ self._makeKarmaCache(
490+ self.person, d_product, [('bugs', 5)])
491+ e_product = self.factory.makeProduct(name='ee')
492+ self._makeKarmaCache(
493+ self.person, e_product, [('bugs', 4)])
494+ f_product = self.factory.makeProduct(name='ff')
495+ self._makeKarmaCache(
496+ self.person, f_product, [('bugs', 3)])
497+ results = removeSecurityProxy(
498+ self.person).getProjectsAndCategoriesContributedTo()
499+ names = [entry['project'].name for entry in results]
500+ self.assertEqual(
501+ ['cc', 'bb', 'aa', 'dd', 'ee'], names)
502+
503+
504 def test_suite():
505 return unittest.TestLoader().loadTestsFromName(__name__)