Merge lp:~sinzui/launchpad/deactivate-all-members-fix-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 12620
Proposed branch: lp:~sinzui/launchpad/deactivate-all-members-fix-0
Merge into: lp:launchpad
Diff against target: 1667 lines (+361/-321)
12 files modified
lib/lp/registry/browser/peoplemerge.py (+4/-1)
lib/lp/registry/configure.zcml (+0/-3)
lib/lp/registry/doc/person-merge.txt (+105/-62)
lib/lp/registry/doc/teammembership.txt (+2/-1)
lib/lp/registry/doc/vocabularies.txt (+160/-91)
lib/lp/registry/interfaces/person.py (+1/-8)
lib/lp/registry/interfaces/teammembership.py (+10/-0)
lib/lp/registry/model/person.py (+1/-87)
lib/lp/registry/model/teammembership.py (+28/-0)
lib/lp/registry/tests/test_person.py (+4/-2)
lib/lp/registry/tests/test_team.py (+0/-65)
lib/lp/registry/tests/test_teammembership.py (+46/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/deactivate-all-members-fix-0
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+53885@code.launchpad.net

Description of the change

deactivateAllMembers() must use TM._cleanTeamParticipation.

    Launchpad bug:
        https://bugs.launchpad.net/bugs/733881
    Pre-implementation: jcsackett
    Test command: ./bin/test -vv \
      -t team_membership -t registry/tests/test_person \
      -t registry/tests/test_team -t doc/person-merge -t peoplemerge \t
      -t doc/teammembership -t doc/vocabularies

Oopses like "AssertionError: sinzui is an indirect member of ubuntu-
translations-coordinators but sinzui is not a participant in any direct member
of ubuntu-translations-coordinators" was caused by a deletion on team. All
the users in the delete team were members of ~rosetta-admins via another
team. The TeamParticipation entries for ~rosetta-admins should not have been
deleted.

Person.deactivateAllMembers() was called many steps before merge was called
to ensure there were no TP entries for the team. This method does a simple
delete of TP based on the active membership. It does not recursively check if
the user has an other path to participation in a team via another team
membership.

TeamMembership._cleanTeamParticipation does does build a exclude list of
participation entries. The fix might be to replace the custom query with a
call to TM._cleanTeamParticipation(child, parent). I see store.invalidate() is
called twice; I think once will suffice. The final storm call to remove the
direct participants may not be needed either.

--------------------------------------------------------------------

RULES

    * Remove the redundant call to store.invalidate().
    * Replace the inline sql with a call to TM._cleanTeamParticipation
    * Remove the code to that deals with the direct participants if
      the master function does it.
    * ADDENDUM: Move this method to ITeamMembershipSet to encourage the
      one true way to manange TeamParticipation

QA

    * On staging or QA, staging, delete ~launchpad-chr.
    * Verify that ~sinzui/+participation still works.

LINT

    lib/lp/registry/configure.zcml
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/doc/person-merge.txt
    lib/lp/registry/doc/teammembership.txt
    lib/lp/registry/doc/vocabularies.txt
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/interfaces/teammembership.py
    lib/lp/registry/model/person.py
    lib/lp/registry/model/teammembership.py
    lib/lp/registry/tests/test_person.py
    lib/lp/registry/tests/test_team.py
    lib/lp/registry/tests/test_teammembership.py
^ lint does not like some of the doctests. I can clean them up before
landing this branch.

IMPLEMENTATION

Added a test to verify the expected behaviour of mass member deactivation.
Refactored deactivateAllMembers to use TM._cleanTeamParticipation, which
allowed me to delete 2/3 of the method. The storm bug mentioned in the
comment was fixed a long time ago, so I fixed the enums. I then moved this
method to TMSet.deactivateActiveMemberships to keep the TeamParticipation
rules in one module. The existing tests from duplicated the existing
tests, but there was no test to verify that the comment and user were
saved with the mass deactivation. I revised one of the old tests to verify
that deactivateActiveMemberships stores the right information.
    lib/lp/registry/interfaces/teammembership.py
    lib/lp/registry/model/teammembership.py
    lib/lp/registry/tests/test_teammembership.py
    lib/lp/registry/tests/test_team.py

Updated call sites to use TMSet.deactivateActiveMemberships. Removed
the old method which also permited the removal of an interface, and the
deletion of an duplicate tests.
    lib/lp/registry/configure.zcml
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/doc/person-merge.txt
    lib/lp/registry/doc/teammembership.txt
    lib/lp/registry/doc/vocabularies.txt
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good to land. Moving the deactivation code into TeamMembership is a big win, and I like how much simpler this makes a fairly complicated process.

Thanks, Curtis.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2011-02-16 17:31:11 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2011-03-17 20:19:01 +0000
@@ -47,6 +47,7 @@
47 IPersonSet,47 IPersonSet,
48 IRequestPeopleMerge,48 IRequestPeopleMerge,
49 )49 )
50from lp.registry.interfaces.teammembership import ITeamMembershipSet
50from lp.services.propertycache import cachedproperty51from lp.services.propertycache import cachedproperty
51from lp.soyuz.enums import ArchiveStatus52from lp.soyuz.enums import ArchiveStatus
52from lp.soyuz.interfaces.archive import IArchiveSet53from lp.soyuz.interfaces.archive import IArchiveSet
@@ -317,7 +318,9 @@
317 'issues with this change.'318 'issues with this change.'
318 % (self.target_person.unique_displayname,319 % (self.target_person.unique_displayname,
319 canonical_url(self.target_person)))320 canonical_url(self.target_person)))
320 self.dupe_person.deactivateAllMembers(comment, self.user)321 membershipset = getUtility(ITeamMembershipSet)
322 membershipset.deactivateActiveMemberships(
323 self.dupe_person, comment, self.user)
321 flush_database_updates()324 flush_database_updates()
322 self.doMerge(data)325 self.doMerge(data)
323326
324327
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2011-03-10 19:11:04 +0000
+++ lib/lp/registry/configure.zcml 2011-03-17 20:19:01 +0000
@@ -893,9 +893,6 @@
893 permission="launchpad.Moderate"893 permission="launchpad.Moderate"
894 set_attributes="name"/>894 set_attributes="name"/>
895 <require895 <require
896 permission="launchpad.Moderate"
897 interface="lp.registry.interfaces.person.IPersonModerate" />
898 <require
899 permission="launchpad.Commercial"896 permission="launchpad.Commercial"
900 set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>897 set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
901 <require898 <require
902899
=== modified file 'lib/lp/registry/doc/person-merge.txt'
--- lib/lp/registry/doc/person-merge.txt 2011-01-04 16:08:57 +0000
+++ lib/lp/registry/doc/person-merge.txt 2011-03-17 20:19:01 +0000
@@ -1,10 +1,11 @@
1= Merging =1Merging
2=======
23
3For many reasons (i.e. a gina run) we could have duplicated accounts in4For many reasons (i.e. a gina run) we could have duplicated accounts in
4Launchpad. Once a duplicated account is identified, we need to allow the user5Launchpad. Once a duplicated account is identified, we need to allow the
5to merge two accounts into a single one, because both represent the same6user to merge two accounts into a single one, because both represent the
6person and they're there just because each of those was created using a7same person and they're there just because each of those was created
7different email address.8using a different email address.
89
9 >>> from zope.component import getUtility10 >>> from zope.component import getUtility
10 >>> from canonical.database.sqlbase import sqlvalues11 >>> from canonical.database.sqlbase import sqlvalues
@@ -19,7 +20,8 @@
19 >>> marilize = personset.getByName('marilize')20 >>> marilize = personset.getByName('marilize')
2021
2122
22== Sanity checks ==23Sanity checks
24-------------
2325
24We can't merge an account that still has email addresses attached to it26We can't merge an account that still has email addresses attached to it
2527
@@ -29,13 +31,15 @@
29 AssertionError: ...31 AssertionError: ...
3032
3133
32== Preparing test person for the merge ==34Preparing test person for the merge
35-----------------------------------
3336
34Merging people involves updating the merged person relationships. Let's37Merging people involves updating the merged person relationships. Let's
35put the person we will merge into some of those.38put the person we will merge into some of those.
3639
37 # To assign marilize as the ubuntu team owner, we must log on as the40 # To assign marilize as the ubuntu team owner, we must log on as the
38 # previous owner.41 # previous owner.
42
39 >>> login('mark@example.com')43 >>> login('mark@example.com')
4044
41 >>> ubuntu_team = personset.getByName('ubuntu-team')45 >>> ubuntu_team = personset.getByName('ubuntu-team')
@@ -55,20 +59,23 @@
55 >>> saved_marilize_karma_id = marilize_karma.id59 >>> saved_marilize_karma_id = marilize_karma.id
56 >>> print marilize_karma.person.name60 >>> print marilize_karma.person.name
57 marilize61 marilize
62
58 >>> sampleperson_old_karma = sample.karma63 >>> sampleperson_old_karma = sample.karma
5964
60Branches whose owner is being merged are uniquified by appending '-N' where N65Branches whose owner is being merged are uniquified by appending '-N'
61is a unique integer. We create "peoplemerge" and "peoplemerge-1" branches owned66where N is a unique integer. We create "peoplemerge" and "peoplemerge-1"
62by marilize, and a "peoplemerge" and "peoplemerge-1" branches owned by 'Sample67branches owned by marilize, and a "peoplemerge" and "peoplemerge-1"
63Person' to test that branch name uniquifying works.68branches owned by 'Sample Person' to test that branch name uniquifying
6469works.
65Branches with smaller IDs will be processed first, so we create "peoplemerge"70
66first, and it will be renamed "peoplemerge-2". The extant "peoplemerge-1"71Branches with smaller IDs will be processed first, so we create
67branch will be renamed "peoplemerge-1-1". The "peoplemerge-0" branch will not72"peoplemerge" first, and it will be renamed "peoplemerge-2". The extant
68be renamed since it will not conflict.73"peoplemerge-1" branch will be renamed "peoplemerge-1-1". The
6974"peoplemerge-0" branch will not be renamed since it will not conflict.
70That is not a particularly sensible way of renaming branches, but it is simple75
71to implement, and it be should extremely rare for the case to occur.76That is not a particularly sensible way of renaming branches, but it is
77simple to implement, and it be should extremely rare for the case to
78occur.
7279
73 >>> peoplemerge = factory.makePersonalBranch(80 >>> peoplemerge = factory.makePersonalBranch(
74 ... name='peoplemerge', owner=sample)81 ... name='peoplemerge', owner=sample)
@@ -81,12 +88,13 @@
81 >>> peoplemerge11 = factory.makePersonalBranch(88 >>> peoplemerge11 = factory.makePersonalBranch(
82 ... name='peoplemerge-1', owner=marilize)89 ... name='peoplemerge-1', owner=marilize)
8390
84'Sample Person' is a deactivated member of the 'Ubuntu Translators' team,91'Sample Person' is a deactivated member of the 'Ubuntu Translators'
85while marilize is an active member. After the merge, 'Sample Person' will be an92team, while marilize is an active member. After the merge, 'Sample
86active member of that team.93Person' will be an active member of that team.
8794
88 >>> sample in ubuntu_translators.inactivemembers95 >>> sample in ubuntu_translators.inactivemembers
89 True96 True
97
90 >>> marilize in ubuntu_translators.activemembers98 >>> marilize in ubuntu_translators.activemembers
91 True99 True
92100
@@ -102,11 +110,13 @@
102 u'Marilize Coetzee'110 u'Marilize Coetzee'
103111
104112
105== Do the merge! ==113Do the merge!
114-------------
106115
107 # Now we remove the only email address marilize had, so that we can merge116 # Now we remove the only email address marilize had, so that we can merge
108 # it. First we need to change its status, though, because we can't delete117 # it. First we need to change its status, though, because we can't delete
109 # a person's preferred email.118 # a person's preferred email.
119
110 >>> from canonical.launchpad.interfaces.emailaddress import (120 >>> from canonical.launchpad.interfaces.emailaddress import (
111 ... EmailAddressStatus)121 ... EmailAddressStatus)
112 >>> from canonical.launchpad.interfaces.lpstorm import IMasterObject122 >>> from canonical.launchpad.interfaces.lpstorm import IMasterObject
@@ -119,13 +129,15 @@
119 >>> personset.merge(marilize, sample)129 >>> personset.merge(marilize, sample)
120130
121131
122== Merge results ==132Merge results
133-------------
123134
124Check that 'Sample Person' has indeed become an active member of 'Ubuntu135Check that 'Sample Person' has indeed become an active member of 'Ubuntu
125Translators'136Translators'
126137
127 >>> sample in ubuntu_translators.activemembers138 >>> sample in ubuntu_translators.activemembers
128 True139 True
140
129 >>> sample.inTeam(ubuntu_translators)141 >>> sample.inTeam(ubuntu_translators)
130 True142 True
131143
@@ -136,27 +148,32 @@
136 >>> sample_junk = get_branch_namespace(sample)148 >>> sample_junk = get_branch_namespace(sample)
137 >>> sample_junk.getByName('peoplemerge') == peoplemerge149 >>> sample_junk.getByName('peoplemerge') == peoplemerge
138 True150 True
151
139 >>> sample_junk.getByName('peoplemerge-0') == peoplemerge0152 >>> sample_junk.getByName('peoplemerge-0') == peoplemerge0
140 True153 True
154
141 >>> sample_junk.getByName('peoplemerge-1') == peoplemerge1155 >>> sample_junk.getByName('peoplemerge-1') == peoplemerge1
142 True156 True
157
143 >>> sample_junk.getByName('peoplemerge-2') == peoplemerge2158 >>> sample_junk.getByName('peoplemerge-2') == peoplemerge2
144 True159 True
160
145 >>> sample_junk.getByName('peoplemerge-1-1') == peoplemerge11161 >>> sample_junk.getByName('peoplemerge-1-1') == peoplemerge11
146 True162 True
147163
148The Karma that was previously assigned to marilize is now assigned to name12164The Karma that was previously assigned to marilize is now assigned to
149(Sample Person).165name12 (Sample Person).
150166
151 >>> from canonical.database.sqlbase import flush_database_caches167 >>> from canonical.database.sqlbase import flush_database_caches
152 >>> flush_database_caches()168 >>> flush_database_caches()
153 >>> saved_marilize_karma_id == marilize_karma.id169 >>> saved_marilize_karma_id == marilize_karma.id
154 True170 True
171
155 >>> print marilize_karma.person.name172 >>> print marilize_karma.person.name
156 name12173 name12
157174
158Note that we don't bother migrating karma caches - it will just be reset next175Note that we don't bother migrating karma caches - it will just be reset
159time the caches are rebuilt.176next time the caches are rebuilt.
160177
161 >>> sample.karma == sampleperson_old_karma178 >>> sample.karma == sampleperson_old_karma
162 True179 True
@@ -199,8 +216,8 @@
199 >>> results.get_one()[0] is None216 >>> results.get_one()[0] is None
200 True217 True
201218
202An email is sent to the user informing him that he should review his email219An email is sent to the user informing him that he should review his
203and mailing list subscription settings.220email and mailing list subscription settings.
204221
205 >>> from lp.registry.interfaces.personnotification import (222 >>> from lp.registry.interfaces.personnotification import (
206 ... IPersonNotificationSet)223 ... IPersonNotificationSet)
@@ -209,11 +226,14 @@
209 >>> notifications = notification_set.getNotificationsToSend()226 >>> notifications = notification_set.getNotificationsToSend()
210 >>> notifications.count()227 >>> notifications.count()
211 1228 1
229
212 >>> notification = notifications[0]230 >>> notification = notifications[0]
213 >>> print notification.person.name231 >>> print notification.person.name
214 name12232 name12
233
215 >>> print notification.subject234 >>> print notification.subject
216 Launchpad accounts merged235 Launchpad accounts merged
236
217 >>> print notification.body237 >>> print notification.body
218 The Launchpad account named 'marilize-merged' was merged into the account238 The Launchpad account named 'marilize-merged' was merged into the account
219 named 'name12'. ...239 named 'name12'. ...
@@ -226,13 +246,13 @@
226Person decoration246Person decoration
227-----------------247-----------------
228248
229Several tables "extend" the Person table by having additional information249Several tables "extend" the Person table by having additional
230that is UNIQUEly keyed to Person.id. We have a utility function that merges250information that is UNIQUEly keyed to Person.id. We have a utility
231information in those tables, we test it here.251function that merges information in those tables, we test it here.
232252
233We will use PersonLocation as an example. There are many permutations and253We will use PersonLocation as an example. There are many permutations
234combinations, we will exercise them all, and in each case we'll create, and254and combinations, we will exercise them all, and in each case we'll
235then delete, the needed two people.255create, and then delete, the needed two people.
236256
237 >>> from lp.registry.model.person import PersonSet, Person257 >>> from lp.registry.model.person import PersonSet, Person
238 >>> from lp.registry.interfaces.person import PersonCreationRationale258 >>> from lp.registry.interfaces.person import PersonCreationRationale
@@ -272,11 +292,12 @@
272 >>> winner, loser = endless_supply_of_players.next()292 >>> winner, loser = endless_supply_of_players.next()
273 >>> print decorator_refs(store, winner, loser)293 >>> print decorator_refs(store, winner, loser)
274 <BLANKLINE>294 <BLANKLINE>
295
275 >>> personset._merge_person_decoration(winner, loser, skip,296 >>> personset._merge_person_decoration(winner, loser, skip,
276 ... 'PersonLocation', 'person', ['last_modified_by',])297 ... 'PersonLocation', 'person', ['last_modified_by',])
277298
278"Skip" should have been updated with the table and unique reference column299"Skip" should have been updated with the table and unique reference
279name.300column name.
280301
281 >>> print skip302 >>> print skip
282 [('personlocation', 'person')]303 [('personlocation', 'person')]
@@ -286,42 +307,44 @@
286 >>> print decorator_refs(store, winner, loser)307 >>> print decorator_refs(store, winner, loser)
287 <BLANKLINE>308 <BLANKLINE>
288309
289OK, now, this time, we will add some decorator information to the winner but310OK, now, this time, we will add some decorator information to the winner
290not the loser.311but not the loser.
291312
292 >>> winner, loser = endless_supply_of_players.next()313 >>> winner, loser = endless_supply_of_players.next()
293 >>> winner.setLocation(None, None, 'America/Santiago', winner)314 >>> winner.setLocation(None, None, 'America/Santiago', winner)
294 >>> print decorator_refs(store, winner, loser)315 >>> print decorator_refs(store, winner, loser)
295 winner, winner,316 winner, winner,
317
296 >>> personset._merge_person_decoration(winner, loser, skip,318 >>> personset._merge_person_decoration(winner, loser, skip,
297 ... 'PersonLocation', 'person', ['last_modified_by',])319 ... 'PersonLocation', 'person', ['last_modified_by',])
298320
299There should now still be one decorator, with all columns pointing to the321There should now still be one decorator, with all columns pointing to
300winner:322the winner:
301323
302 >>> print decorator_refs(store, winner, loser)324 >>> print decorator_refs(store, winner, loser)
303 winner, winner,325 winner, winner,
304326
305This time, we will have a decorator for the person that is being merged INTO327This time, we will have a decorator for the person that is being merged
306another person, but nothing on the target person.328INTO another person, but nothing on the target person.
307329
308 >>> winner, loser = endless_supply_of_players.next()330 >>> winner, loser = endless_supply_of_players.next()
309 >>> loser.setLocation(None, None, 'America/Santiago', loser)331 >>> loser.setLocation(None, None, 'America/Santiago', loser)
310 >>> print decorator_refs(store, winner, loser)332 >>> print decorator_refs(store, winner, loser)
311 loser, loser,333 loser, loser,
334
312 >>> personset._merge_person_decoration(winner, loser, skip,335 >>> personset._merge_person_decoration(winner, loser, skip,
313 ... 'PersonLocation', 'person', ['last_modified_by',])336 ... 'PersonLocation', 'person', ['last_modified_by',])
314337
315There should now still be one decorator, with all columns pointing to the338There should now still be one decorator, with all columns pointing to
316winner:339the winner:
317340
318 >>> print decorator_refs(store, winner, loser)341 >>> print decorator_refs(store, winner, loser)
319 winner, winner,342 winner, winner,
320343
321Now, we want to show what happens when there is a decorator for both the344Now, we want to show what happens when there is a decorator for both the
322to_person and the from_person. We expect that the from_person record will345to_person and the from_person. We expect that the from_person record
323remain as noise but non-unique columns will have been updated to point to346will remain as noise but non-unique columns will have been updated to
324the winner, and the to_person will be unaffected.347point to the winner, and the to_person will be unaffected.
325348
326 >>> winner, loser = endless_supply_of_players.next()349 >>> winner, loser = endless_supply_of_players.next()
327 >>> winner.setLocation(None, None, 'America/Santiago', winner)350 >>> winner.setLocation(None, None, 'America/Santiago', winner)
@@ -329,6 +352,7 @@
329 >>> print decorator_refs(store, winner, loser)352 >>> print decorator_refs(store, winner, loser)
330 winner, winner,353 winner, winner,
331 loser, loser,354 loser, loser,
355
332 >>> personset._merge_person_decoration(winner, loser, skip,356 >>> personset._merge_person_decoration(winner, loser, skip,
333 ... 'PersonLocation', 'person', ['last_modified_by',])357 ... 'PersonLocation', 'person', ['last_modified_by',])
334 >>> print decorator_refs(store, winner, loser)358 >>> print decorator_refs(store, winner, loser)
@@ -336,12 +360,13 @@
336 loser, winner,360 loser, winner,
337361
338362
339== Merging teams ==363Merging teams
364-------------
340365
341Merging of teams is also possible and uses the same API used for merging366Merging of teams is also possible and uses the same API used for merging
342people. Note, though, that when merging teams, its polls will not be367people. Note, though, that when merging teams, its polls will not be
343carried over to the remaining team. Team memberships, on the other hand,368carried over to the remaining team. Team memberships, on the other
344are carried over just like when merging people.369hand, are carried over just like when merging people.
345370
346 >>> from datetime import datetime, timedelta371 >>> from datetime import datetime, timedelta
347 >>> import pytz372 >>> import pytz
@@ -357,37 +382,45 @@
357 ... PollSecrecy.OPEN, allowspoilt=True)382 ... PollSecrecy.OPEN, allowspoilt=True)
358383
359 # test_team has a superteam, one active member and a poll.384 # test_team has a superteam, one active member and a poll.
385
360 >>> [team.name for team in test_team.super_teams]386 >>> [team.name for team in test_team.super_teams]
361 [u'launchpad']387 [u'launchpad']
388
362 >>> test_team.teamowner.name389 >>> test_team.teamowner.name
363 u'name12'390 u'name12'
391
364 >>> [member.name for member in test_team.allmembers]392 >>> [member.name for member in test_team.allmembers]
365 [u'name12']393 [u'name12']
394
366 >>> list(IPollSubset(test_team).getAll())395 >>> list(IPollSubset(test_team).getAll())
367 [<Poll at ...]396 [<Poll at ...]
368397
369 # Landscape-developers has no super teams, two members and no polls.398 # Landscape-developers has no super teams, two members and no polls.
399
370 >>> landscape = personset.getByName('landscape-developers')400 >>> landscape = personset.getByName('landscape-developers')
371 >>> [team.name for team in landscape.super_teams]401 >>> [team.name for team in landscape.super_teams]
372 []402 []
403
373 >>> landscape.teamowner.name404 >>> landscape.teamowner.name
374 u'name12'405 u'name12'
406
375 >>> [member.name for member in landscape.allmembers]407 >>> [member.name for member in landscape.allmembers]
376 [u'salgado', u'name12']408 [u'salgado', u'name12']
409
377 >>> list(IPollSubset(landscape).getAll())410 >>> list(IPollSubset(landscape).getAll())
378 []411 []
379412
380Now we try to merge them, but since test_team has active members it can't be413Now we try to merge them, but since test_team has active members it
381merged.414can't be merged.
382415
383 >>> personset.merge(test_team, landscape)416 >>> personset.merge(test_team, landscape)
384 Traceback (most recent call last):417 Traceback (most recent call last):
385 ...418 ...
386 AssertionError: Only teams without active members can be merged419 AssertionError: Only teams without active members can be merged
387420
388A team with a mailing list can only be merged if has no mailing list or if the421A team with a mailing list can only be merged if has no mailing list or
389mailing list is in the PURGED state. This state indicates that there are no422if the mailing list is in the PURGED state. This state indicates that
390artifacts of the list on the Mailman side.423there are no artifacts of the list on the Mailman side.
391424
392 >>> from lp.registry.interfaces.mailinglist import IMailingListSet425 >>> from lp.registry.interfaces.mailinglist import IMailingListSet
393 >>> mailing_list = getUtility(IMailingListSet).new(landscape)426 >>> mailing_list = getUtility(IMailingListSet).new(landscape)
@@ -395,21 +428,23 @@
395 >>> mailman.act()428 >>> mailman.act()
396 >>> print mailing_list.status.name429 >>> print mailing_list.status.name
397 ACTIVE430 ACTIVE
431
398 >>> ubuntu_team = personset.getByName('ubuntu-team')432 >>> ubuntu_team = personset.getByName('ubuntu-team')
399 >>> personset.merge(landscape, ubuntu_team)433 >>> personset.merge(landscape, ubuntu_team)
400 Traceback (most recent call last):434 Traceback (most recent call last):
401 ...435 ...
402 AssertionError: Can't merge teams which have mailing lists...436 AssertionError: Can't merge teams which have mailing lists...
403437
404Purging the associated mailing list doesn't delete it, but puts it into a438Purging the associated mailing list doesn't delete it, but puts it into
405state that essentially treats it as nonexistent. It still can't be merged439a state that essentially treats it as nonexistent. It still can't be
406until all the members are deactivated though.440merged until all the members are deactivated though.
407441
408 >>> mailing_list.deactivate()442 >>> mailing_list.deactivate()
409 >>> mailman.act()443 >>> mailman.act()
410 >>> mailing_list.purge()444 >>> mailing_list.purge()
411 >>> print mailing_list.status.name445 >>> print mailing_list.status.name
412 PURGED446 PURGED
447
413 >>> personset.merge(landscape, ubuntu_team)448 >>> personset.merge(landscape, ubuntu_team)
414 Traceback (most recent call last):449 Traceback (most recent call last):
415 ...450 ...
@@ -418,26 +453,34 @@
418For cases like this we have an API which takes care of deactivating all453For cases like this we have an API which takes care of deactivating all
419active members of the team so that we can perform the merge.454active members of the team so that we can perform the merge.
420455
456 >>> from lp.registry.interfaces.teammembership import ITeamMembershipSet
457
421 # Only admins can use that API, though, so we need to login as an admin.458 # Only admins can use that API, though, so we need to login as an admin.
459
422 >>> login('foo.bar@canonical.com')460 >>> login('foo.bar@canonical.com')
423461
424 >>> comment = "We're merging this team into another one."462 >>> comment = "We're merging this team into another one."
425 >>> test_team.deactivateAllMembers(comment, personset.getByName('name16'))463 >>> membershipset = getUtility(ITeamMembershipSet)
464 >>> membershipset.deactivateActiveMemberships(
465 ... test_team, comment, personset.getByName('name16'))
426 >>> for team in test_team.super_teams:466 >>> for team in test_team.super_teams:
427 ... test_team.retractTeamMembership(team, test_team.teamowner)467 ... test_team.retractTeamMembership(team, test_team.teamowner)
428468
429
430 >>> personset.merge(test_team, landscape)469 >>> personset.merge(test_team, landscape)
431470
432 # The resulting Landscape-developers no new super teams, has471 # The resulting Landscape-developers no new super teams, has
433 # no polls and its members are still the same two from before the472 # no polls and its members are still the same two from before the
434 # merge.473 # merge.
474
435 >>> landscape.teamowner.name475 >>> landscape.teamowner.name
436 u'name12'476 u'name12'
477
437 >>> [member.name for member in landscape.allmembers]478 >>> [member.name for member in landscape.allmembers]
438 [u'salgado', u'name12']479 [u'salgado', u'name12']
480
439 >>> [team.name for team in landscape.super_teams]481 >>> [team.name for team in landscape.super_teams]
440 []482 []
483
441 >>> list(IPollSubset(landscape).getAll())484 >>> list(IPollSubset(landscape).getAll())
442 []485 []
443486
444487
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2011-02-28 23:25:52 +0000
+++ lib/lp/registry/doc/teammembership.txt 2011-03-17 20:19:01 +0000
@@ -895,7 +895,8 @@
895If a team is merged it will not show up in the set of administered teams.895If a team is merged it will not show up in the set of administered teams.
896896
897 >>> login('foo.bar@canonical.com')897 >>> login('foo.bar@canonical.com')
898 >>> cprov_team.deactivateAllMembers("Merging", foobar)898 >>> membershipset.deactivateActiveMemberships(
899 ... cprov_team, "Merging", foobar)
899 >>> personset.merge(cprov_team, guadamen_team)900 >>> personset.merge(cprov_team, guadamen_team)
900 >>> [team.name for team in cprov.getAdministratedTeams()]901 >>> [team.name for team in cprov.getAdministratedTeams()]
901 [u'canonical-partner-dev', u'guadamen', u'launchpad-buildd-admins']902 [u'canonical-partner-dev', u'guadamen', u'launchpad-buildd-admins']
902903
=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt 2010-12-21 21:59:19 +0000
+++ lib/lp/registry/doc/vocabularies.txt 2011-03-17 20:19:01 +0000
@@ -1,4 +1,5 @@
1= Registry vocabularies =1Registry vocabularies
2=====================
23
3 >>> from canonical.database.sqlbase import flush_database_updates4 >>> from canonical.database.sqlbase import flush_database_updates
4 >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag5 >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
@@ -21,16 +22,18 @@
21 >>> product_vocabulary = get_naked_vocab(None, "Product")22 >>> product_vocabulary = get_naked_vocab(None, "Product")
2223
2324
24== ActiveMailingList ==25ActiveMailingList
26-----------------
2527
26The active mailing lists vocabulary matches and returns only those mailing28The active mailing lists vocabulary matches and returns only those
27lists which are active.29mailing lists which are active.
2830
29 >>> list_vocabulary = get_naked_vocab(None, 'ActiveMailingList')31 >>> list_vocabulary = get_naked_vocab(None, 'ActiveMailingList')
30 >>> from canonical.launchpad.webapp.testing import verifyObject32 >>> from canonical.launchpad.webapp.testing import verifyObject
31 >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary33 >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
32 >>> verifyObject(IHugeVocabulary, list_vocabulary)34 >>> verifyObject(IHugeVocabulary, list_vocabulary)
33 True35 True
36
34 >>> list_vocabulary.displayname37 >>> list_vocabulary.displayname
35 'Select an active mailing list.'38 'Select an active mailing list.'
3639
@@ -38,17 +41,15 @@
3841
39 >>> list(list_vocabulary)42 >>> list(list_vocabulary)
40 []43 []
44
41 >>> len(list_vocabulary)45 >>> len(list_vocabulary)
42 046 0
47
43 >>> list(list_vocabulary.search())48 >>> list(list_vocabulary.search())
44 []49 []
4550
46Mailing lists are not active when they are first registered.51Mailing lists are not active when they are first registered.
4752
48 >>> # These are the convoluted steps to create some mailing lists. We
49 >>> # can't use the shortcuts that other related tests use because those
50 >>> # leave the list in the ACTIVE state and we want to check things
51 >>> # before they get to that state.
52 >>> personset = getUtility(IPersonSet)53 >>> personset = getUtility(IPersonSet)
53 >>> ddaa = personset.getByName('ddaa')54 >>> ddaa = personset.getByName('ddaa')
54 >>> carlos = personset.getByName('carlos')55 >>> carlos = personset.getByName('carlos')
@@ -70,13 +71,15 @@
70 >>> list_three = listset.new(team_three)71 >>> list_three = listset.new(team_three)
71 >>> list(list_vocabulary)72 >>> list(list_vocabulary)
72 []73 []
74
73 >>> len(list_vocabulary)75 >>> len(list_vocabulary)
74 076 0
77
75 >>> list(list_vocabulary.search())78 >>> list(list_vocabulary.search())
76 []79 []
7780
78Mailing lists become active once they have been constructed by Mailman (which81Mailing lists become active once they have been constructed by Mailman
79indicates so by transitioning the state to ACTIVE).82(which indicates so by transitioning the state to ACTIVE).
8083
81 >>> list_one.startConstructing()84 >>> list_one.startConstructing()
82 >>> list_two.startConstructing()85 >>> list_two.startConstructing()
@@ -88,11 +91,12 @@
88 >>> sorted(mailing_list.team.displayname91 >>> sorted(mailing_list.team.displayname
89 ... for mailing_list in list_vocabulary)92 ... for mailing_list in list_vocabulary)
90 [u'Bass Players', u'Drummers', u'Guitar Players']93 [u'Bass Players', u'Drummers', u'Guitar Players']
94
91 >>> len(list_vocabulary)95 >>> len(list_vocabulary)
92 396 3
9397
94Searching for active lists is done through the vocabulary as well. With a98Searching for active lists is done through the vocabulary as well. With
95search term of None, all active lists are returned.99a search term of None, all active lists are returned.
96100
97 >>> sorted(mailing_list.team.displayname101 >>> sorted(mailing_list.team.displayname
98 ... for mailing_list in list_vocabulary.search(None))102 ... for mailing_list in list_vocabulary.search(None))
@@ -104,27 +108,31 @@
104 ... for mailing_list in list_vocabulary.search('player'))108 ... for mailing_list in list_vocabulary.search('player'))
105 [u'Bass Players', u'Guitar Players']109 [u'Bass Players', u'Guitar Players']
106110
107The IHugeVocabulary interface also requires a search method that returns a111The IHugeVocabulary interface also requires a search method that returns
108CountableIterator.112a CountableIterator.
109113
110 >>> iter = list_vocabulary.searchForTerms('player')114 >>> iter = list_vocabulary.searchForTerms('player')
111 >>> from canonical.launchpad.webapp.vocabulary import CountableIterator115 >>> from canonical.launchpad.webapp.vocabulary import CountableIterator
112 >>> isinstance(iter, CountableIterator)116 >>> isinstance(iter, CountableIterator)
113 True117 True
118
114 >>> sorted((term.value.team.name, term.token, term.title)119 >>> sorted((term.value.team.name, term.token, term.title)
115 ... for term in iter)120 ... for term in iter)
116 [(u'bass-players', 'bass-players', u'Bass Players'),121 [(u'bass-players', 'bass-players', u'Bass Players'),
117 (u'guitar-players', 'guitar-players', u'Guitar Players')]122 (u'guitar-players', 'guitar-players', u'Guitar Players')]
118123
119The vocabulary supports accessing mailing lists by 'term', where the term must124The vocabulary supports accessing mailing lists by 'term', where the
120be a mailing list. The returned term's value is the mailing list object, the125term must be a mailing list. The returned term's value is the mailing
121token is the team name and the title is the team's display name.126list object, the token is the team name and the title is the team's
127display name.
122128
123 >>> term_1 = list_vocabulary.getTerm(list_two)129 >>> term_1 = list_vocabulary.getTerm(list_two)
124 >>> term_1.value.team.displayname130 >>> term_1.value.team.displayname
125 u'Guitar Players'131 u'Guitar Players'
132
126 >>> term_1.token133 >>> term_1.token
127 'guitar-players'134 'guitar-players'
135
128 >>> term_1.title136 >>> term_1.title
129 u'Guitar Players'137 u'Guitar Players'
130138
@@ -140,13 +148,14 @@
140 >>> term_2 = list_vocabulary.getTermByToken(term_1.token)148 >>> term_2 = list_vocabulary.getTermByToken(term_1.token)
141 >>> term_2.value.team.displayname149 >>> term_2.value.team.displayname
142 u'Guitar Players'150 u'Guitar Players'
151
143 >>> term_3 = list_vocabulary.getTerm(list_one)152 >>> term_3 = list_vocabulary.getTerm(list_one)
144 >>> term_4 = list_vocabulary.getTermByToken(term_3.token)153 >>> term_4 = list_vocabulary.getTermByToken(term_3.token)
145 >>> term_4.value.team.displayname154 >>> term_4.value.team.displayname
146 u'Bass Players'155 u'Bass Players'
147156
148If you try to get the term by a token not represented in the vocabulary, you157If you try to get the term by a token not represented in the vocabulary,
149get an exception.158you get an exception.
150159
151 >>> list_vocabulary.getTermByToken('turntablists')160 >>> list_vocabulary.getTermByToken('turntablists')
152 Traceback (most recent call last):161 Traceback (most recent call last):
@@ -158,8 +167,8 @@
158 >>> list_three in list_vocabulary167 >>> list_three in list_vocabulary
159 True168 True
160169
161You are not allowed to ask whether a non-mailing list object is contained in170You are not allowed to ask whether a non-mailing list object is
162this vocabulary.171contained in this vocabulary.
163172
164 >>> team_three in list_vocabulary173 >>> team_three in list_vocabulary
165 Traceback (most recent call last):174 Traceback (most recent call last):
@@ -179,11 +188,13 @@
179188
180 >>> list(list_vocabulary.search('flautists'))189 >>> list(list_vocabulary.search('flautists'))
181 []190 []
191
182 >>> list(list_vocabulary.search('cellists'))192 >>> list(list_vocabulary.search('cellists'))
183 []193 []
184194
185195
186=== DistroSeriesVocabulary ===196DistroSeriesVocabulary
197......................
187198
188Reflects the available distribution series. Results are ordered by199Reflects the available distribution series. Results are ordered by
189`name`200`name`
@@ -227,7 +238,8 @@
227 []238 []
228239
229240
230=== PersonActiveMembership ===241PersonActiveMembership
242......................
231243
232All the teams the person is an active member of.244All the teams the person is an active member of.
233245
@@ -236,6 +248,7 @@
236 ... foo_bar, 'PersonActiveMembership')248 ... foo_bar, 'PersonActiveMembership')
237 >>> len(person_active_membership)249 >>> len(person_active_membership)
238 10250 10
251
239 >>> for term in person_active_membership:252 >>> for term in person_active_membership:
240 ... print term.token, term.value.displayname, term.title253 ... print term.token, term.value.displayname, term.title
241 canonical-partner-dev Canonical Partner Developers254 canonical-partner-dev Canonical Partner Developers
@@ -253,6 +266,7 @@
253 >>> launchpad_team = person_set.getByName('launchpad')266 >>> launchpad_team = person_set.getByName('launchpad')
254 >>> launchpad_team in person_active_membership267 >>> launchpad_team in person_active_membership
255 True268 True
269
256 >>> mirrors_admins = person_set.getByName('mirrors-admins')270 >>> mirrors_admins = person_set.getByName('mirrors-admins')
257 >>> mirrors_admins in person_active_membership271 >>> mirrors_admins in person_active_membership
258 False272 False
@@ -313,7 +327,8 @@
313 LookupError:...327 LookupError:...
314328
315329
316=== PersonTeamParticipations ===330PersonTeamParticipations
331........................
317332
318This vocabulary contains all the teams a person participates in. Either333This vocabulary contains all the teams a person participates in. Either
319through direct or indirect participations.334through direct or indirect participations.
@@ -322,6 +337,7 @@
322 >>> [membership.team.name337 >>> [membership.team.name
323 ... for membership in sample_person.team_memberships]338 ... for membership in sample_person.team_memberships]
324 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']339 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']
340
325 >>> [team.name for team in sample_person.teams_participated_in]341 >>> [team.name for team in sample_person.teams_participated_in]
326 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',342 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',
327 u'name20']343 u'name20']
@@ -338,16 +354,17 @@
338 name20: Warty Security Team (name20)354 name20: Warty Security Team (name20)
339355
340356
341=== Milestone ===357Milestone
358.........
342359
343All the milestone in a context.360All the milestone in a context.
344361
345A MilestoneVolcabulary contains different milestones, depending on the362A MilestoneVolcabulary contains different milestones, depending on the
346current context. It is pointless to present the large number of all363current context. It is pointless to present the large number of all
347active milestones known in Launchpad in a vocabulary. Hence a364active milestones known in Launchpad in a vocabulary. Hence a
348MilestoneVolcabulary contains only those milestones that are related365MilestoneVolcabulary contains only those milestones that are related to
349to the current context. If no context is given, or if the context does366the current context. If no context is given, or if the context does not
350not have any milestones, a MilestoneVocabulary is empty...367have any milestones, a MilestoneVocabulary is empty...
351368
352 >>> milestones = get_naked_vocab(None, 'Milestone')369 >>> milestones = get_naked_vocab(None, 'Milestone')
353 >>> len(milestones)370 >>> len(milestones)
@@ -359,14 +376,15 @@
359 >>> len(milestones)376 >>> len(milestones)
360 0377 0
361378
362...but if the context is an IPerson, the MilestoneVocabulary contains all379...but if the context is an IPerson, the MilestoneVocabulary contains
363milestones. IPerson related pages showing milestone lists retrieve the380all milestones. IPerson related pages showing milestone lists retrieve
364milestones from RelevantMilestonesMixin.getMilestoneWidgetValues()381the milestones from RelevantMilestonesMixin.getMilestoneWidgetValues()
365but we need the big default vocabulary for form input validation.382but we need the big default vocabulary for form input validation.
366383
367 >>> all_milestones = get_naked_vocab(sample_person, 'Milestone')384 >>> all_milestones = get_naked_vocab(sample_person, 'Milestone')
368 >>> len(all_milestones)385 >>> len(all_milestones)
369 3386 3
387
370 >>> for term in all_milestones:388 >>> for term in all_milestones:
371 ... print "%s: %s" % (term.value.target.name, term.value.name)389 ... print "%s: %s" % (term.value.target.name, term.value.name)
372 debian: 3.1390 debian: 3.1
@@ -403,6 +421,7 @@
403 >>> firefox_task = bug_one.bugtasks[0]421 >>> firefox_task = bug_one.bugtasks[0]
404 >>> firefox_task.bugtargetdisplayname422 >>> firefox_task.bugtargetdisplayname
405 u'Mozilla Firefox'423 u'Mozilla Firefox'
424
406 >>> firefox_task_milestones = get_naked_vocab(425 >>> firefox_task_milestones = get_naked_vocab(
407 ... firefox_task, 'Milestone')426 ... firefox_task, 'Milestone')
408 >>> for term in firefox_task_milestones:427 >>> for term in firefox_task_milestones:
@@ -414,14 +433,15 @@
414 >>> debian_woody_task = bug_two.bugtasks[-1]433 >>> debian_woody_task = bug_two.bugtasks[-1]
415 >>> debian_woody_task.bugtargetdisplayname434 >>> debian_woody_task.bugtargetdisplayname
416 u'mozilla-firefox (Debian Woody)'435 u'mozilla-firefox (Debian Woody)'
436
417 >>> debian_woody_milestones = get_naked_vocab(437 >>> debian_woody_milestones = get_naked_vocab(
418 ... debian_woody_task, 'Milestone')438 ... debian_woody_task, 'Milestone')
419 >>> debian_woody = debian_woody_task.distroseries439 >>> debian_woody = debian_woody_task.distroseries
420 >>> len(debian_woody_milestones)440 >>> len(debian_woody_milestones)
421 2441 2
422442
423If one of the milestones is disabled, it won't be included in the vocabulary443If one of the milestones is disabled, it won't be included in the
424anymore.444vocabulary anymore.
425445
426 >>> milestone = debian_woody.milestones[0]446 >>> milestone = debian_woody.milestones[0]
427 >>> milestone.active = False447 >>> milestone.active = False
@@ -429,8 +449,9 @@
429 >>> len(get_naked_vocab(debian_woody_task, 'Milestone'))449 >>> len(get_naked_vocab(debian_woody_task, 'Milestone'))
430 1450 1
431451
432If the milestone was used in a bugtask before it was marked inactive, though,452If the milestone was used in a bugtask before it was marked inactive,
433it'll still show up on the vocabulary so that users can change it.453though, it'll still show up on the vocabulary so that users can change
454it.
434455
435 >>> debian_woody_task.milestone = milestone456 >>> debian_woody_task.milestone = milestone
436 >>> flush_database_updates()457 >>> flush_database_updates()
@@ -466,8 +487,8 @@
466 firefox: 1.0487 firefox: 1.0
467 firefox: firefox-milestone-no-series488 firefox: firefox-milestone-no-series
468489
469If the context is a specification, only milestones from that specification490If the context is a specification, only milestones from that
470target are in the vocabulary.491specification target are in the vocabulary.
471492
472 >>> canvas_spec = firefox.getSpecification('canvas')493 >>> canvas_spec = firefox.getSpecification('canvas')
473 >>> spec_target_milestones = get_naked_vocab(494 >>> spec_target_milestones = get_naked_vocab(
@@ -485,18 +506,21 @@
485 >>> one_dot_o = firefox.milestones[0]506 >>> one_dot_o = firefox.milestones[0]
486 >>> one_dot_o.name507 >>> one_dot_o.name
487 u'1.0'508 u'1.0'
509
488 >>> one_dot_o.active = False510 >>> one_dot_o.active = False
489511
490 >>> firefox_milestones = get_naked_vocab(firefox, 'Milestone')512 >>> firefox_milestones = get_naked_vocab(firefox, 'Milestone')
491 >>> len(firefox_milestones)513 >>> len(firefox_milestones)
492 0514 0
515
493 >>> firefox_task_milestones = get_naked_vocab(516 >>> firefox_task_milestones = get_naked_vocab(
494 ... firefox_task, 'Milestone')517 ... firefox_task, 'Milestone')
495 >>> len(firefox_task_milestones)518 >>> len(firefox_task_milestones)
496 0519 0
497520
498521
499=== ProjectProductsVocabulary ===522ProjectProductsVocabulary
523.........................
500524
501All the products in a project.525All the products in a project.
502526
@@ -510,7 +534,8 @@
510 thunderbird: Mozilla Thunderbird534 thunderbird: Mozilla Thunderbird
511535
512536
513=== ProjectGroupVocabulary ===537ProjectGroupVocabulary
538......................
514539
515The list of selectable projects. The results are ordered by displayname.540The list of selectable projects. The results are ordered by displayname.
516541
@@ -520,6 +545,7 @@
520545
521 >>> [p.title for p in project_vocabulary.search('mozilla')]546 >>> [p.title for p in project_vocabulary.search('mozilla')]
522 [u'The Mozilla Project']547 [u'The Mozilla Project']
548
523 >>> mozilla = project_vocabulary.getTermByToken('mozilla')549 >>> mozilla = project_vocabulary.getTermByToken('mozilla')
524 >>> mozilla.title550 >>> mozilla.title
525 u'The Mozilla Project'551 u'The Mozilla Project'
@@ -533,6 +559,7 @@
533559
534 >>> [p.title for p in project_vocabulary.search('mozilla')]560 >>> [p.title for p in project_vocabulary.search('mozilla')]
535 [u'The Mozilla Project']561 [u'The Mozilla Project']
562
536 >>> moz_project.active = False563 >>> moz_project.active = False
537 >>> flush_database_updates()564 >>> flush_database_updates()
538 >>> moz_project in project_vocabulary565 >>> moz_project in project_vocabulary
@@ -540,11 +567,13 @@
540567
541 >>> [p.title for p in project_vocabulary.search('mozilla')]568 >>> [p.title for p in project_vocabulary.search('mozilla')]
542 []569 []
570
543 >>> moz_project.active = True571 >>> moz_project.active = True
544 >>> flush_database_updates()572 >>> flush_database_updates()
545573
546574
547=== ProductReleaseVocabulary ===575ProductReleaseVocabulary
576........................
548577
549The list of selectable products releases.578The list of selectable products releases.
550579
@@ -554,6 +583,7 @@
554583
555 >>> list(productrelease_vocabulary.search(None))584 >>> list(productrelease_vocabulary.search(None))
556 []585 []
586
557 >>> evolution_releases = productrelease_vocabulary.search("evolution")587 >>> evolution_releases = productrelease_vocabulary.search("evolution")
558 >>> l = [release_term.title for release_term in evolution_releases]588 >>> l = [release_term.title for release_term in evolution_releases]
559 >>> release = productrelease_vocabulary.getTermByToken(589 >>> release = productrelease_vocabulary.getTermByToken(
@@ -562,10 +592,11 @@
562 u'evolution trunk 2.1.6'592 u'evolution trunk 2.1.6'
563593
564594
565=== PersonAccountToMergeVocabulary ===595PersonAccountToMergeVocabulary
596..............................
566597
567All non-merged people with at least one email address. This vocabulary is598All non-merged people with at least one email address. This vocabulary
568meant to be used only in the people merge form.599is meant to be used only in the people merge form.
569600
570 >>> vocab = get_naked_vocab(None, "PersonAccountToMerge")601 >>> vocab = get_naked_vocab(None, "PersonAccountToMerge")
571 >>> vocab.displayname602 >>> vocab.displayname
@@ -576,8 +607,8 @@
576 >>> list(vocab.search(None))607 >>> list(vocab.search(None))
577 []608 []
578609
579Searching for 'Launchpad Administrators' will return an empty list, because610Searching for 'Launchpad Administrators' will return an empty list,
580teams are not part of this vocabulary.611because teams are not part of this vocabulary.
581612
582 >>> [item.name for item in list(vocab.search('Launchpad Administrators'))]613 >>> [item.name for item in list(vocab.search('Launchpad Administrators'))]
583 []614 []
@@ -632,6 +663,7 @@
632 True663 True
633664
634 # Here we cheat because IPerson.merged is a readonly attribute.665 # Here we cheat because IPerson.merged is a readonly attribute.
666
635 >>> naked_cprov = removeSecurityProxy(cprov)667 >>> naked_cprov = removeSecurityProxy(cprov)
636 >>> naked_cprov.merged = 1668 >>> naked_cprov.merged = 1
637 >>> naked_cprov.syncUpdate()669 >>> naked_cprov.syncUpdate()
@@ -653,7 +685,8 @@
653 True685 True
654686
655687
656== AdminMergeablePerson ==688AdminMergeablePerson
689--------------------
657690
658The set of non-merged people.691The set of non-merged people.
659692
@@ -661,18 +694,21 @@
661 >>> vocab.displayname694 >>> vocab.displayname
662 'Select a Person to Merge'695 'Select a Person to Merge'
663696
664Unlike PersonAccountToMerge, this vocabulary includes people who don't have a697Unlike PersonAccountToMerge, this vocabulary includes people who don't
665single email address, as it's fine for admins to merge them.698have a single email address, as it's fine for admins to merge them.
666699
667 >>> print fooperson.preferredemail700 >>> print fooperson.preferredemail
668 None701 None
702
669 >>> list(fooperson.validatedemails) + list(fooperson.guessedemails)703 >>> list(fooperson.validatedemails) + list(fooperson.guessedemails)
670 []704 []
705
671 >>> fooperson in vocab706 >>> fooperson in vocab
672 True707 True
673708
674709
675=== NonMergedPeopleAndTeams ===710NonMergedPeopleAndTeams
711.......................
676712
677All non-merged people and teams.713All non-merged people and teams.
678714
@@ -683,8 +719,8 @@
683 >>> list(vocab.search(None))719 >>> list(vocab.search(None))
684 []720 []
685721
686This vocabulary includes both validated and unvalidated profiles, as well722This vocabulary includes both validated and unvalidated profiles, as
687as teams:723well as teams:
688724
689 >>> [(p.name, p.is_valid_person) for p in vocab.search('matsubara')]725 >>> [(p.name, p.is_valid_person) for p in vocab.search('matsubara')]
690 [(u'matsubara', False)]726 [(u'matsubara', False)]
@@ -704,12 +740,13 @@
704 False740 False
705741
706742
707=== ValidPersonOrTeam ===743ValidPersonOrTeam
744.................
708745
709All 'valid' persons or teams. This is currently defined as people with a746All 'valid' persons or teams. This is currently defined as people with a
710password, a preferred email address and not merged (Person.merged is747password, a preferred email address and not merged (Person.merged is
711None). It also includes all public teams and private teams the748None). It also includes all public teams and private teams the user has
712user has permission to view.749permission to view.
713750
714 >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam")751 >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam")
715 >>> vocab.displayname752 >>> vocab.displayname
@@ -723,11 +760,12 @@
723760
724 >>> vocab.getTermByToken('name16').value.displayname761 >>> vocab.getTermByToken('name16').value.displayname
725 u'Foo Bar'762 u'Foo Bar'
763
726 >>> vocab.getTermByToken('foo.bar@canonical.com').value.displayname764 >>> vocab.getTermByToken('foo.bar@canonical.com').value.displayname
727 u'Foo Bar'765 u'Foo Bar'
728766
729Almost all teams have the word 'team' as part of their names, so a search767Almost all teams have the word 'team' as part of their names, so a
730for 'team' should give us some of them. Notice that the768search for 'team' should give us some of them. Notice that the
731PRIVATE_TEAM 'myteam' is not included in the results.769PRIVATE_TEAM 'myteam' is not included in the results.
732770
733 >>> login_person(sample_person)771 >>> login_person(sample_person)
@@ -740,8 +778,11 @@
740778
741Valid teams do not include teams that have been merged.779Valid teams do not include teams that have been merged.
742780
781 >>> from lp.registry.interfaces.teammembership import ITeamMembershipSet
743 >>> login_person(foo_bar)782 >>> login_person(foo_bar)
744 >>> ephemeral.deactivateAllMembers("Merging", foo_bar)783 >>> membershipset = getUtility(ITeamMembershipSet)
784 >>> membershipset.deactivateActiveMemberships(
785 ... ephemeral, "Merging", foo_bar)
745 >>> person_set.merge(ephemeral, foo_bar)786 >>> person_set.merge(ephemeral, foo_bar)
746 >>> login_person(sample_person)787 >>> login_person(sample_person)
747 >>> sorted(person.name for person in vocab.search('team'))788 >>> sorted(person.name for person in vocab.search('team'))
@@ -750,7 +791,8 @@
750 u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team',791 u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team',
751 u'warty-gnome']792 u'warty-gnome']
752793
753A PRIVATE team is displayed when the logged in user is a member of the team.794A PRIVATE team is displayed when the logged in user is a member of the
795team.
754796
755 >>> commercial = person_set.getByEmail('commercial-member@canonical.com')797 >>> commercial = person_set.getByEmail('commercial-member@canonical.com')
756 >>> vocab = get_naked_vocab(commercial, "ValidPersonOrTeam")798 >>> vocab = get_naked_vocab(commercial, "ValidPersonOrTeam")
@@ -776,7 +818,8 @@
776 u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team',818 u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team',
777 u'warty-gnome']819 u'warty-gnome']
778820
779The PRIVATE team can be looked up via getTermByToken for a member of the team.821The PRIVATE team can be looked up via getTermByToken for a member of the
822team.
780823
781 >>> term = vocab.getTermByToken('private-team')824 >>> term = vocab.getTermByToken('private-team')
782 >>> print term.title825 >>> print term.title
@@ -818,15 +861,16 @@
818 [...u'private-team'...]861 [...u'private-team'...]
819862
820A search for 'support' will give us only the persons which have support863A search for 'support' will give us only the persons which have support
821as part of their name or displayname, or the beginning of864as part of their name or displayname, or the beginning of one of its
822one of its email addresses.865email addresses.
823866
824 >>> login('foo.bar@canonical.com')867 >>> login('foo.bar@canonical.com')
825 >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam")868 >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam")
826 >>> sorted(person.name for person in vocab.search('support'))869 >>> sorted(person.name for person in vocab.search('support'))
827 [u'ubuntu-team']870 [u'ubuntu-team']
828871
829Matsubara doesn't have a preferred email address; he's not a valid Person.872Matsubara doesn't have a preferred email address; he's not a valid
873Person.
830874
831 >>> sorted(person.name for person in vocab.search('matsubara'))875 >>> sorted(person.name for person in vocab.search('matsubara'))
832 []876 []
@@ -841,13 +885,14 @@
841 >>> [cjwatson] = vocab.search('cjwatson')885 >>> [cjwatson] = vocab.search('cjwatson')
842 >>> cjwatson.name, cjwatson.preferredemail.email886 >>> cjwatson.name, cjwatson.preferredemail.email
843 (u'kamion', u'colin.watson@ubuntulinux.com')887 (u'kamion', u'colin.watson@ubuntulinux.com')
888
844 >>> [ircid.nickname for ircid in cjwatson.ircnicknames]889 >>> [ircid.nickname for ircid in cjwatson.ircnicknames]
845 [u'cjwatson']890 [u'cjwatson']
846891
847Since there are so many people and teams a vocabulary that includes892Since there are so many people and teams a vocabulary that includes them
848them all is not very useful when displaying in the user interface. So893all is not very useful when displaying in the user interface. So we
849we limit the number of results. The results are ordered by894limit the number of results. The results are ordered by displayname and
850displayname and the first set of those are the ones returned895the first set of those are the ones returned
851896
852 >>> login(ANONYMOUS)897 >>> login(ANONYMOUS)
853 >>> [person.displayname for person in vocab.search('team')]898 >>> [person.displayname for person in vocab.search('team')]
@@ -859,10 +904,12 @@
859 >>> login(ANONYMOUS)904 >>> login(ANONYMOUS)
860 >>> vocab.LIMIT905 >>> vocab.LIMIT
861 100906 100
907
862 >>> vocab.LIMIT = 4908 >>> vocab.LIMIT = 4
863909
864 # The limit gets applied in multiple subselects, so the result910 # The limit gets applied in multiple subselects, so the result
865 # can actually be less than the limit.911 # can actually be less than the limit.
912
866 >>> [person.displayname for person in vocab.search('team')]913 >>> [person.displayname for person in vocab.search('team')]
867 [u'HWDB Team', u'No Team Memberships', u'testing Spanish team']914 [u'HWDB Team', u'No Team Memberships', u'testing Spanish team']
868915
@@ -910,15 +957,17 @@
910 u'ubuntu-team', u'warty-gnome']957 u'ubuntu-team', u'warty-gnome']
911958
912959
913=== ValidTeam ===960ValidTeam
961.........
914962
915The valid team vocabulary is just like the ValidPersonOrTeam vocabulary,963The valid team vocabulary is just like the ValidPersonOrTeam vocabulary,
916except that its terms are limited only to teams. No non-team Persons will be964except that its terms are limited only to teams. No non-team Persons
917returned.965will be returned.
918966
919 >>> vocab = get_naked_vocab(None, 'ValidTeam')967 >>> vocab = get_naked_vocab(None, 'ValidTeam')
920 >>> vocab.displayname968 >>> vocab.displayname
921 'Select a Team'969 'Select a Team'
970
922 >>> sorted((team.displayname, team.teamowner.displayname)971 >>> sorted((team.displayname, team.teamowner.displayname)
923 ... for team in vocab.search(None))972 ... for team in vocab.search(None))
924 [(u'Bass Players', u'David Allouche'),973 [(u'Bass Players', u'David Allouche'),
@@ -956,8 +1005,9 @@
956 (u'Warty Security Team', u'Mark Shuttleworth'),1005 (u'Warty Security Team', u'Mark Shuttleworth'),
957 (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')]1006 (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')]
9581007
959Like with ValidPersonOrTeam, you can narrow your search down by providing some1008Like with ValidPersonOrTeam, you can narrow your search down by
960text to match against the team name. Still, you only get teams back.1009providing some text to match against the team name. Still, you only get
1010teams back.
9611011
962 >>> sorted((team.displayname, team.teamowner.displayname)1012 >>> sorted((team.displayname, team.teamowner.displayname)
963 ... for team in vocab.search('spanish'))1013 ... for team in vocab.search('spanish'))
@@ -987,7 +1037,8 @@
987 (u'Warty Security Team', u'Mark Shuttleworth'),1037 (u'Warty Security Team', u'Mark Shuttleworth'),
988 (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')]1038 (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')]
9891039
990A user who is a member of a private team will see that team in his search.1040A user who is a member of a private team will see that team in his
1041search.
9911042
992 >>> login('commercial-member@canonical.com')1043 >>> login('commercial-member@canonical.com')
993 >>> sorted((team.displayname, team.teamowner.displayname)1044 >>> sorted((team.displayname, team.teamowner.displayname)
@@ -1011,7 +1062,8 @@
1011 [(u'Ubuntu Team', u'Mark Shuttleworth')]1062 [(u'Ubuntu Team', u'Mark Shuttleworth')]
10121063
10131064
1014=== ValidPerson ===1065ValidPerson
1066...........
10151067
1016All 'valid' persons who are not a team.1068All 'valid' persons who are not a team.
10171069
@@ -1019,19 +1071,23 @@
1019 >>> vocab = get_naked_vocab(None, "ValidPerson")1071 >>> vocab = get_naked_vocab(None, "ValidPerson")
1020 >>> vocab.displayname1072 >>> vocab.displayname
1021 'Select a Person'1073 'Select a Person'
1074
1022 >>> people = vocab.search(None)1075 >>> people = vocab.search(None)
1023 >>> people.count() > 01076 >>> people.count() > 0
1024 True1077 True
1078
1025 >>> invalid_people = [1079 >>> invalid_people = [
1026 ... person for person in people if not person.is_valid_person]1080 ... person for person in people if not person.is_valid_person]
1027 >>> print len(invalid_people)1081 >>> print len(invalid_people)
1028 01082 0
10291083
1030There are two 'Carlos' in the sample data but only one is a valid person.1084There are two 'Carlos' in the sample data but only one is a valid
1085person.
10311086
1032 >>> carlos_people = vocab.search('Carlos')1087 >>> carlos_people = vocab.search('Carlos')
1033 >>> print len(list(carlos_people))1088 >>> print len(list(carlos_people))
1034 11089 1
1090
1035 >>> invalid_carlos = [1091 >>> invalid_carlos = [
1036 ... person for person in carlos_people if not person.is_valid_person]1092 ... person for person in carlos_people if not person.is_valid_person]
1037 >>> print len(invalid_carlos)1093 >>> print len(invalid_carlos)
@@ -1039,29 +1095,27 @@
10391095
1040ValidPerson does not include teams.1096ValidPerson does not include teams.
10411097
1042 >>> # Create a new team.
1043 >>> carlos = getUtility(IPersonSet).getByName('carlos')1098 >>> carlos = getUtility(IPersonSet).getByName('carlos')
1044 >>> carlos_team = factory.makeTeam(1099 >>> carlos_team = factory.makeTeam(
1045 ... owner=carlos, name='carlos-team')1100 ... owner=carlos, name='carlos-team')
1046 >>> person_or_team_vocab = get_naked_vocab(None, "ValidPersonOrTeam")1101 >>> person_or_team_vocab = get_naked_vocab(None, "ValidPersonOrTeam")
1047 >>> carlos_people_or_team = person_or_team_vocab.search('carlos')1102 >>> carlos_people_or_team = person_or_team_vocab.search('carlos')
1048 >>> # The people or team search yields our one Carlos person and
1049 >>> # the new team.
1050 >>> print len(list(carlos_people_or_team))1103 >>> print len(list(carlos_people_or_team))
1051 21104 2
1105
1052 >>> carlos_team in carlos_people_or_team1106 >>> carlos_team in carlos_people_or_team
1053 True1107 True
10541108
1055 >>> # But the ValidPersonVocabulary only has the original Carlos
1056 >>> # person, not the new team.
1057 >>> carlos_people = vocab.search('carlos')1109 >>> carlos_people = vocab.search('carlos')
1058 >>> print len(list(carlos_people))1110 >>> print len(list(carlos_people))
1059 11111 1
1112
1060 >>> carlos_team in carlos_people1113 >>> carlos_team in carlos_people
1061 False1114 False
10621115
10631116
1064=== DistributionOrProductVocabulary ===1117DistributionOrProductVocabulary
1118...............................
10651119
1066All products and distributions. Note that the value type is1120All products and distributions. Note that the value type is
1067heterogeneous.1121heterogeneous.
@@ -1077,12 +1131,14 @@
10771131
1078 >>> vocab.getTermByToken('firefox').token1132 >>> vocab.getTermByToken('firefox').token
1079 'firefox'1133 'firefox'
1134
1080 >>> login('mark@example.com')1135 >>> login('mark@example.com')
1081 >>> product_set['firefox'].setAliases(['iceweasel'])1136 >>> product_set['firefox'].setAliases(['iceweasel'])
1082 >>> current_user = launchbag.user1137 >>> current_user = launchbag.user
1083 >>> login_person(current_user)1138 >>> login_person(current_user)
1084 >>> vocab.getTermByToken('iceweasel').token1139 >>> vocab.getTermByToken('iceweasel').token
1085 'firefox'1140 'firefox'
1141
1086 >>> [term.token for term in vocab.searchForTerms(query='iceweasel')]1142 >>> [term.token for term in vocab.searchForTerms(query='iceweasel')]
1087 ['firefox']1143 ['firefox']
10881144
@@ -1101,14 +1157,17 @@
1101 ... if 'Tomcat' in term.title:1157 ... if 'Tomcat' in term.title:
1102 ... print term.title, '- class', term.value.__class__.__name__1158 ... print term.title, '- class', term.value.__class__.__name__
1103 Tomcat (Product) - class Product1159 Tomcat (Product) - class Product
1160
1104 >>> tomcat = product_set.getByName('tomcat')1161 >>> tomcat = product_set.getByName('tomcat')
1105 >>> tomcat in vocab1162 >>> tomcat in vocab
1106 True1163 True
1164
1107 >>> tomcat.active = False1165 >>> tomcat.active = False
1108 >>> flush_database_updates()1166 >>> flush_database_updates()
1109 >>> vocab = get_naked_vocab(None, "DistributionOrProduct")1167 >>> vocab = get_naked_vocab(None, "DistributionOrProduct")
1110 >>> tomcat in vocab1168 >>> tomcat in vocab
1111 False1169 False
1170
1112 >>> tomcat.active = True1171 >>> tomcat.active = True
1113 >>> flush_database_updates()1172 >>> flush_database_updates()
1114 >>> vocab = get_naked_vocab(None, "DistributionOrProduct")1173 >>> vocab = get_naked_vocab(None, "DistributionOrProduct")
@@ -1122,10 +1181,11 @@
1122 False1181 False
11231182
11241183
1125=== DistributionOrProductOrProjectGroupVocabulary ===1184DistributionOrProductOrProjectGroupVocabulary
1185.............................................
11261186
1127All products, project groups and distributions. Note that the value type is1187All products, project groups and distributions. Note that the value type
1128heterogeneous.1188is heterogeneous.
11291189
1130 >>> vocab = get_naked_vocab(None, "DistributionOrProductOrProjectGroup")1190 >>> vocab = get_naked_vocab(None, "DistributionOrProductOrProjectGroup")
1131 >>> for term in vocab:1191 >>> for term in vocab:
@@ -1138,6 +1198,7 @@
11381198
1139 >>> vocab.getTermByToken('ubuntu').token1199 >>> vocab.getTermByToken('ubuntu').token
1140 'ubuntu'1200 'ubuntu'
1201
1141 >>> from lp.registry.interfaces.distribution import (1202 >>> from lp.registry.interfaces.distribution import (
1142 ... IDistributionSet)1203 ... IDistributionSet)
1143 >>> login('mark@example.com')1204 >>> login('mark@example.com')
@@ -1145,6 +1206,7 @@
1145 >>> login_person(current_user)1206 >>> login_person(current_user)
1146 >>> vocab.getTermByToken('ubantoo').token1207 >>> vocab.getTermByToken('ubantoo').token
1147 'ubuntu'1208 'ubuntu'
1209
1148 >>> [term.token for term in vocab.searchForTerms(query='ubantoo')]1210 >>> [term.token for term in vocab.searchForTerms(query='ubantoo')]
1149 ['ubuntu']1211 ['ubuntu']
11501212
@@ -1153,6 +1215,7 @@
1153 >>> tomcat = product_set.getByName('tomcat')1215 >>> tomcat = product_set.getByName('tomcat')
1154 >>> tomcat in vocab1216 >>> tomcat in vocab
1155 True1217 True
1218
1156 >>> tomcat.active = False1219 >>> tomcat.active = False
1157 >>> tomcat in vocab1220 >>> tomcat in vocab
1158 False1221 False
@@ -1160,6 +1223,7 @@
1160 >>> apache = getUtility(IProjectGroupSet).getByName('apache')1223 >>> apache = getUtility(IProjectGroupSet).getByName('apache')
1161 >>> apache in vocab1224 >>> apache in vocab
1162 True1225 True
1226
1163 >>> apache.active = False1227 >>> apache.active = False
1164 >>> apache in vocab1228 >>> apache in vocab
1165 False1229 False
@@ -1179,13 +1243,15 @@
1179 ... if 'Apache' in term.title:1243 ... if 'Apache' in term.title:
1180 ... print term.title, '- class', term.value.__class__.__name__1244 ... print term.title, '- class', term.value.__class__.__name__
1181 Apache (ProjectGroup) - class ProjectGroup1245 Apache (ProjectGroup) - class ProjectGroup
1246
1182 >>> for term in vocab:1247 >>> for term in vocab:
1183 ... if 'Tomcat' in term.title:1248 ... if 'Tomcat' in term.title:
1184 ... print term.title, '- class', term.value.__class__.__name__1249 ... print term.title, '- class', term.value.__class__.__name__
1185 Tomcat (Product) - class Product1250 Tomcat (Product) - class Product
11861251
11871252
1188== FeaturedProjectVocabulary ==1253FeaturedProjectVocabulary
1254-------------------------
11891255
1190The featured project vocabulary contains all the projects that are1256The featured project vocabulary contains all the projects that are
1191featured on Launchpad. It is a subset of the1257featured on Launchpad. It is a subset of the
@@ -1220,19 +1286,20 @@
1220 False1286 False
12211287
12221288
1223== CommercialProjectsVocabulary ==1289CommercialProjectsVocabulary
1290----------------------------
12241291
1225The commercial projects vocabulary contains all commercial projects,1292The commercial projects vocabulary contains all commercial projects,
1226ordered by displayname. Note: a project is considered commercial if1293ordered by displayname. Note: a project is considered commercial if it
1227it has a proprietary license or no license. That's why some of these1294has a proprietary license or no license. That's why some of these
1228clearly FOSS project in our test data show up as commercial.1295clearly FOSS project in our test data show up as commercial.
12291296
1230For a normal user (one who does not have launchpad.Commercial1297For a normal user (one who does not have launchpad.Commercial
1231permission) the owned commercial project vocabulary is a list of1298permission) the owned commercial project vocabulary is a list of project
1232project the user either owns or manages.1299the user either owns or manages.
12331300
1234The test data has one project with a proprietary license. Let's1301The test data has one project with a proprietary license. Let's change
1235change bzr's so we will get more interesting results.1302bzr's so we will get more interesting results.
12361303
1237 >>> from lp.registry.interfaces.product import License1304 >>> from lp.registry.interfaces.product import License
1238 >>> bzr = product_set.getByName('bzr')1305 >>> bzr = product_set.getByName('bzr')
@@ -1284,3 +1351,5 @@
1284 ... print term.value.displayname1351 ... print term.value.displayname
1285 Bazaar1352 Bazaar
1286 Mega Money Maker1353 Mega Money Maker
1354
1355
12871356
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2011-03-01 09:41:39 +0000
+++ lib/lp/registry/interfaces/person.py 2011-03-17 20:19:01 +0000
@@ -1716,13 +1716,6 @@
1716 """1716 """
17171717
17181718
1719class IPersonModerate(Interface):
1720 """IPerson attributes that require launchpad.Moderate."""
1721
1722 def deactivateAllMembers(comment, reviewer):
1723 """Deactivate all the members of this team."""
1724
1725
1726class IPersonCommAdminWriteRestricted(Interface):1719class IPersonCommAdminWriteRestricted(Interface):
1727 """IPerson attributes that require launchpad.Admin permission to set."""1720 """IPerson attributes that require launchpad.Admin permission to set."""
17281721
@@ -1771,7 +1764,7 @@
17711764
1772class IPerson(IPersonPublic, IPersonViewRestricted, IPersonEditRestricted,1765class IPerson(IPersonPublic, IPersonViewRestricted, IPersonEditRestricted,
1773 IPersonCommAdminWriteRestricted, IPersonSpecialRestricted,1766 IPersonCommAdminWriteRestricted, IPersonSpecialRestricted,
1774 IPersonModerate, IHasStanding, ISetLocation, IRootContext):1767 IHasStanding, ISetLocation, IRootContext):
1775 """A Person."""1768 """A Person."""
1776 export_as_webservice_entry(plural_name='people')1769 export_as_webservice_entry(plural_name='people')
17771770
17781771
=== modified file 'lib/lp/registry/interfaces/teammembership.py'
--- lib/lp/registry/interfaces/teammembership.py 2011-03-01 03:52:30 +0000
+++ lib/lp/registry/interfaces/teammembership.py 2011-03-17 20:19:01 +0000
@@ -302,6 +302,16 @@
302 TeamMembership and I'll return None.302 TeamMembership and I'll return None.
303 """303 """
304304
305 def deactivateActiveMemberships(team, comment, reviewer):
306 """Deactivate all team members in ACTIVE_STATES.
307
308 This is a convenience method used before teams are deleted.
309
310 :param team: The team to deactivate.
311 :param comment: An explanation for the deactivation.
312 :param reviewer: The user doing the deactivation.
313 """
314
305315
306class ITeamParticipation(Interface):316class ITeamParticipation(Interface):
307 """A TeamParticipation.317 """A TeamParticipation.
308318
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/model/person.py 2011-03-17 20:19:01 +0000
@@ -246,10 +246,7 @@
246 SSHKeyCompromisedError,246 SSHKeyCompromisedError,
247 SSHKeyType,247 SSHKeyType,
248 )248 )
249from lp.registry.interfaces.teammembership import (249from lp.registry.interfaces.teammembership import TeamMembershipStatus
250 ACTIVE_STATES,
251 TeamMembershipStatus,
252 )
253from lp.registry.interfaces.wikiname import (250from lp.registry.interfaces.wikiname import (
254 IWikiName,251 IWikiName,
255 IWikiNameSet,252 IWikiNameSet,
@@ -1547,89 +1544,6 @@
1547 tm.dateexpires += timedelta(days=team.defaultrenewalperiod)1544 tm.dateexpires += timedelta(days=team.defaultrenewalperiod)
1548 tm.sendSelfRenewalNotification()1545 tm.sendSelfRenewalNotification()
15491546
1550 def deactivateAllMembers(self, comment, reviewer):
1551 """Deactivate all members of this team.
1552
1553 This method circuments the TeamMembership.setStatus() method
1554 to improve performance; therefore, it does not send out any
1555 status change noticiations to the team admins.
1556
1557 :param comment: Explanation of the change.
1558 :param reviewer: Person who made the change.
1559 """
1560 assert self.is_team, "This method is only available for teams."
1561 now = datetime.now(pytz.timezone('UTC'))
1562 store = Store.of(self)
1563 cur = cursor()
1564
1565 # Deactivate the approved/admin team members.
1566 # XXX: EdwinGrubbs 2009-07-08 bug=397072
1567 # There are problems using storm to write an update
1568 # statement using DBITems in the comparison.
1569 cur.execute("""
1570 UPDATE TeamMembership
1571 SET status=%(status)s,
1572 last_changed_by=%(last_changed_by)s,
1573 last_change_comment=%(comment)s,
1574 date_last_changed=%(date_last_changed)s
1575 WHERE
1576 TeamMembership.team = %(team)s
1577 AND TeamMembership.status IN %(original_statuses)s
1578 """,
1579 dict(
1580 status=TeamMembershipStatus.DEACTIVATED,
1581 last_changed_by=reviewer.id,
1582 comment=comment,
1583 date_last_changed=now,
1584 team=self.id,
1585 original_statuses=(
1586 TeamMembershipStatus.ADMIN.value,
1587 TeamMembershipStatus.APPROVED.value)))
1588
1589 # Since we've updated the database behind Storm's back,
1590 # flush its caches.
1591 store.invalidate()
1592
1593 # Remove all indirect TeamParticipation entries resulting from this
1594 # team. If this were just a select, it would be a complicated but
1595 # feasible set of joins. Since it's a delete, we have to use
1596 # some sub selects.
1597 cur.execute('''
1598 DELETE FROM TeamParticipation
1599 WHERE
1600 -- The person needs to be a member of the team in question
1601 person IN
1602 (SELECT person from TeamParticipation WHERE
1603 team = %(team)s) AND
1604
1605 -- The teams being deleted should be teams that this team
1606 -- is a member of.
1607 team IN
1608 (SELECT team from TeamMembership WHERE
1609 person = %(team)s) AND
1610
1611 -- The person needs to not have direct membership in the
1612 -- team.
1613 NOT EXISTS
1614 (SELECT tm1.person from TeamMembership tm1
1615 WHERE
1616 tm1.person = TeamParticipation.person and
1617 tm1.team = TeamParticipation.team and
1618 tm1.status IN %(active_states)s);
1619 ''', dict(team=self.id, active_states=ACTIVE_STATES))
1620
1621 # Since we've updated the database behind Storm's back yet again,
1622 # we need to flush its caches, again.
1623 store.invalidate()
1624
1625 # Remove all members from the TeamParticipation table
1626 # except for the team, itself.
1627 participants = store.find(
1628 TeamParticipation,
1629 TeamParticipation.teamID == self.id,
1630 TeamParticipation.personID != self.id)
1631 participants.remove()
1632
1633 def setMembershipData(self, person, status, reviewer, expires=None,1547 def setMembershipData(self, person, status, reviewer, expires=None,
1634 comment=None):1548 comment=None):
1635 """See `IPerson`."""1549 """See `IPerson`."""
16361550
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/model/teammembership.py 2011-03-17 20:19:01 +0000
@@ -30,6 +30,7 @@
30from canonical.database.datetimecol import UtcDateTimeCol30from canonical.database.datetimecol import UtcDateTimeCol
31from canonical.database.enumcol import EnumCol31from canonical.database.enumcol import EnumCol
32from canonical.database.sqlbase import (32from canonical.database.sqlbase import (
33 cursor,
33 flush_database_updates,34 flush_database_updates,
34 SQLBase,35 SQLBase,
35 sqlvalues,36 sqlvalues,
@@ -493,6 +494,33 @@
493 TeamMembershipRenewalPolicy.AUTOMATIC)494 TeamMembershipRenewalPolicy.AUTOMATIC)
494 return IStore(TeamMembership).find(TeamMembership, *conditions)495 return IStore(TeamMembership).find(TeamMembership, *conditions)
495496
497 def deactivateActiveMemberships(self, team, comment, reviewer):
498 """See `ITeamMembershipSet`."""
499 now = datetime.now(pytz.timezone('UTC'))
500 store = Store.of(team)
501 cur = cursor()
502 all_members = list(team.activemembers)
503 cur.execute("""
504 UPDATE TeamMembership
505 SET status=%(status)s,
506 last_changed_by=%(last_changed_by)s,
507 last_change_comment=%(comment)s,
508 date_last_changed=%(date_last_changed)s
509 WHERE
510 TeamMembership.team = %(team)s
511 AND TeamMembership.status IN %(original_statuses)s
512 """,
513 dict(
514 status=TeamMembershipStatus.DEACTIVATED,
515 last_changed_by=reviewer.id,
516 comment=comment,
517 date_last_changed=now,
518 team=team.id,
519 original_statuses=ACTIVE_STATES))
520 for member in all_members:
521 # store.invalidate() is called for each iteration.
522 _cleanTeamParticipation(member, team)
523
496524
497class TeamParticipation(SQLBase):525class TeamParticipation(SQLBase):
498 implements(ITeamParticipation)526 implements(ITeamParticipation)
499527
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-03-08 22:31:58 +0000
+++ lib/lp/registry/tests/test_person.py 2011-03-17 20:19:01 +0000
@@ -55,6 +55,7 @@
55 PersonVisibility,55 PersonVisibility,
56 )56 )
57from lp.registry.interfaces.product import IProductSet57from lp.registry.interfaces.product import IProductSet
58from lp.registry.interfaces.teammembership import ITeamMembershipSet
58from lp.registry.model.karma import (59from lp.registry.model.karma import (
59 KarmaCategory,60 KarmaCategory,
60 KarmaTotalCache,61 KarmaTotalCache,
@@ -684,8 +685,9 @@
684 self.assertEqual(oldest_date, person.datecreated)685 self.assertEqual(oldest_date, person.datecreated)
685686
686 def _doMerge(self, test_team, target_team):687 def _doMerge(self, test_team, target_team):
687 test_team.deactivateAllMembers(688 membershipset = getUtility(ITeamMembershipSet)
688 comment='',689 membershipset.deactivateActiveMemberships(
690 test_team, comment='',
689 reviewer=test_team.teamowner)691 reviewer=test_team.teamowner)
690 self.person_set.merge(test_team, target_team)692 self.person_set.merge(test_team, target_team)
691693
692694
=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 2011-02-01 19:16:43 +0000
+++ lib/lp/registry/tests/test_team.py 2011-03-17 20:19:01 +0000
@@ -425,68 +425,3 @@
425 members = list(team.approvedmembers)425 members = list(team.approvedmembers)
426 self.assertEqual(1, len(members))426 self.assertEqual(1, len(members))
427 self.assertEqual(user, members[0])427 self.assertEqual(user, members[0])
428
429
430class TestMembershipManagement(TestCaseWithFactory):
431
432 layer = DatabaseFunctionalLayer
433
434 def test_deactivateAllMembers_cleans_up_teamparticipation_deactivated(
435 self):
436 superteam = self.factory.makeTeam(name='super')
437 targetteam = self.factory.makeTeam(name='target')
438 login_celebrity('admin')
439 targetteam.join(superteam, targetteam.teamowner)
440
441 # Now we create a deactivated link for the target team's teamowner.
442 targetteam.teamowner.join(superteam, targetteam.teamowner)
443 targetteam.teamowner.leave(superteam)
444
445 self.assertEqual(
446 sorted([superteam, targetteam]),
447 sorted([team for team in
448 targetteam.teamowner.teams_participated_in]))
449 targetteam.deactivateAllMembers(
450 comment='test',
451 reviewer=targetteam.teamowner)
452 self.assertEqual(
453 [],
454 sorted([team for team in
455 targetteam.teamowner.teams_participated_in]))
456
457 def test_deactivateAllMembers_cleans_up_teamparticipation_teamowner(self):
458 superteam = self.factory.makeTeam(name='super')
459 targetteam = self.factory.makeTeam(name='target')
460 login_celebrity('admin')
461 targetteam.join(superteam, targetteam.teamowner)
462 self.assertEqual(
463 sorted([superteam, targetteam]),
464 sorted([team for team
465 in targetteam.teamowner.teams_participated_in]))
466 targetteam.deactivateAllMembers(
467 comment='test',
468 reviewer=targetteam.teamowner)
469 self.assertEqual(
470 [],
471 sorted([team for team
472 in targetteam.teamowner.teams_participated_in]))
473
474 def test_deactivateAllMembers_cleans_up_team_participation(self):
475 superteam = self.factory.makeTeam(name='super')
476 sharedteam = self.factory.makeTeam(name='shared')
477 anotherteam = self.factory.makeTeam(name='another')
478 targetteam = self.factory.makeTeam(name='target')
479 person = self.factory.makePerson()
480 login_celebrity('admin')
481 person.join(targetteam)
482 person.join(sharedteam)
483 person.join(anotherteam)
484 targetteam.join(superteam, targetteam.teamowner)
485 targetteam.join(sharedteam, targetteam.teamowner)
486 self.assertTrue(superteam in person.teams_participated_in)
487 targetteam.deactivateAllMembers(
488 comment='test',
489 reviewer=targetteam.teamowner)
490 self.assertEqual(
491 sorted([sharedteam, anotherteam]),
492 sorted([team for team in person.teams_participated_in]))
493428
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2011-03-01 03:52:30 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2011-03-17 20:19:01 +0000
@@ -51,16 +51,18 @@
51 TeamParticipation,51 TeamParticipation,
52 )52 )
53from lp.testing import (53from lp.testing import (
54 login_celebrity,
54 person_logged_in,55 person_logged_in,
55 TestCaseWithFactory,56 TestCaseWithFactory,
56 )57 )
57from lp.testing.mail_helpers import pop_notifications58from lp.testing.mail_helpers import pop_notifications
5859
5960
60class TestTeamMembershipSet(TestCase):61class TestTeamMembershipSet(TestCaseWithFactory):
61 layer = DatabaseFunctionalLayer62 layer = DatabaseFunctionalLayer
6263
63 def setUp(self):64 def setUp(self):
65 super(TestTeamMembershipSet, self).setUp()
64 login('test@canonical.com')66 login('test@canonical.com')
65 self.membershipset = getUtility(ITeamMembershipSet)67 self.membershipset = getUtility(ITeamMembershipSet)
66 self.personset = getUtility(IPersonSet)68 self.personset = getUtility(IPersonSet)
@@ -166,6 +168,24 @@
166 sample_person_on_motu.status, TeamMembershipStatus.EXPIRED)168 sample_person_on_motu.status, TeamMembershipStatus.EXPIRED)
167 self.failIf(sample_person.inTeam(motu))169 self.failIf(sample_person.inTeam(motu))
168170
171 def test_deactivateActiveMemberships(self):
172 superteam = self.factory.makeTeam(name='super')
173 targetteam = self.factory.makeTeam(name='target')
174 member = self.factory.makePerson()
175 login_celebrity('admin')
176 targetteam.join(superteam, targetteam.teamowner)
177 targetteam.addMember(member, targetteam.teamowner)
178 targetteam.teamowner.join(superteam, targetteam.teamowner)
179 self.membershipset.deactivateActiveMemberships(
180 targetteam, comment='test', reviewer=targetteam.teamowner)
181 membership = self.membershipset.getByPersonAndTeam(member, targetteam)
182 self.assertEqual('test', membership.last_change_comment)
183 self.assertEqual(targetteam.teamowner, membership.last_changed_by)
184 self.assertEqual([], list(targetteam.allmembers))
185 self.assertEqual(
186 [superteam], list(targetteam.teamowner.teams_participated_in))
187 self.assertEqual([], list(member.teams_participated_in))
188
169189
170class TeamParticipationTestCase(TestCaseWithFactory):190class TeamParticipationTestCase(TestCaseWithFactory):
171 """Tests for team participation using 5 teams."""191 """Tests for team participation using 5 teams."""
@@ -465,6 +485,31 @@
465 previous_count-10,485 previous_count-10,
466 self.getTeamParticipationCount())486 self.getTeamParticipationCount())
467487
488 def testTeam3_deactivateActiveMemberships(self):
489 # Removing all the members of team2 will not remove memberships
490 # to super teams from other paths.
491 non_member = self.factory.makePerson()
492 self.team3.addMember(non_member, self.foo_bar, force_team_add=True)
493 previous_count = self.getTeamParticipationCount()
494 membershipset = getUtility(ITeamMembershipSet)
495 membershipset.deactivateActiveMemberships(
496 self.team3, 'gone', self.foo_bar)
497 self.assertEqual([], list(self.team3.allmembers))
498 self.assertParticipantsEquals(
499 ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
500 self.team1)
501 self.assertParticipantsEquals(
502 ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2)
503 self.assertParticipantsEquals(
504 [], self.team3)
505 self.assertParticipantsEquals(
506 ['name16', 'no-priv', 'team5'], self.team4)
507 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
508 self.assertParticipantsEquals(
509 ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'],
510 self.team6)
511 self.assertEqual(previous_count - 8, self.getTeamParticipationCount())
512
468513
469class TestTeamMembership(TestCaseWithFactory):514class TestTeamMembership(TestCaseWithFactory):
470 layer = DatabaseFunctionalLayer515 layer = DatabaseFunctionalLayer