Merge lp:~sinzui/launchpad/deactivate-all-members-fix-0 into lp:launchpad
- deactivate-all-members-fix-0
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
j.c.sackett (community) | Approve | ||
Review via email: mp+53885@code.launchpad.net |
Commit message
Description of the change
deactivateAllMe
Launchpad bug:
https:/
Pre-
Test command: ./bin/test -vv \
-t team_membership -t registry/
-t registry/
-t doc/teammembership -t doc/vocabularies
Oopses like "AssertionError: sinzui is an indirect member of ubuntu-
translations-
of ubuntu-
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.
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.
participation entries. The fix might be to replace the custom query with a
call to TM._cleanTeamPa
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._cleanTeamPa
* 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/
LINT
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
^ 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 deactivateAllMe
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.deactivat
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 deactivateActiv
lib/
lib/
lib/
lib/
Updated call sites to use TMSet.deactivat
the old method which also permited the removal of an interface, and the
deletion of an duplicate tests.
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
Preview Diff
1 | === modified file 'lib/lp/registry/browser/peoplemerge.py' | |||
2 | --- lib/lp/registry/browser/peoplemerge.py 2011-02-16 17:31:11 +0000 | |||
3 | +++ lib/lp/registry/browser/peoplemerge.py 2011-03-17 20:19:01 +0000 | |||
4 | @@ -47,6 +47,7 @@ | |||
5 | 47 | IPersonSet, | 47 | IPersonSet, |
6 | 48 | IRequestPeopleMerge, | 48 | IRequestPeopleMerge, |
7 | 49 | ) | 49 | ) |
8 | 50 | from lp.registry.interfaces.teammembership import ITeamMembershipSet | ||
9 | 50 | from lp.services.propertycache import cachedproperty | 51 | from lp.services.propertycache import cachedproperty |
10 | 51 | from lp.soyuz.enums import ArchiveStatus | 52 | from lp.soyuz.enums import ArchiveStatus |
11 | 52 | from lp.soyuz.interfaces.archive import IArchiveSet | 53 | from lp.soyuz.interfaces.archive import IArchiveSet |
12 | @@ -317,7 +318,9 @@ | |||
13 | 317 | 'issues with this change.' | 318 | 'issues with this change.' |
14 | 318 | % (self.target_person.unique_displayname, | 319 | % (self.target_person.unique_displayname, |
15 | 319 | canonical_url(self.target_person))) | 320 | canonical_url(self.target_person))) |
17 | 320 | self.dupe_person.deactivateAllMembers(comment, self.user) | 321 | membershipset = getUtility(ITeamMembershipSet) |
18 | 322 | membershipset.deactivateActiveMemberships( | ||
19 | 323 | self.dupe_person, comment, self.user) | ||
20 | 321 | flush_database_updates() | 324 | flush_database_updates() |
21 | 322 | self.doMerge(data) | 325 | self.doMerge(data) |
22 | 323 | 326 | ||
23 | 324 | 327 | ||
24 | === modified file 'lib/lp/registry/configure.zcml' | |||
25 | --- lib/lp/registry/configure.zcml 2011-03-10 19:11:04 +0000 | |||
26 | +++ lib/lp/registry/configure.zcml 2011-03-17 20:19:01 +0000 | |||
27 | @@ -893,9 +893,6 @@ | |||
28 | 893 | permission="launchpad.Moderate" | 893 | permission="launchpad.Moderate" |
29 | 894 | set_attributes="name"/> | 894 | set_attributes="name"/> |
30 | 895 | <require | 895 | <require |
31 | 896 | permission="launchpad.Moderate" | ||
32 | 897 | interface="lp.registry.interfaces.person.IPersonModerate" /> | ||
33 | 898 | <require | ||
34 | 899 | permission="launchpad.Commercial" | 896 | permission="launchpad.Commercial" |
35 | 900 | set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/> | 897 | set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/> |
36 | 901 | <require | 898 | <require |
37 | 902 | 899 | ||
38 | === modified file 'lib/lp/registry/doc/person-merge.txt' | |||
39 | --- lib/lp/registry/doc/person-merge.txt 2011-01-04 16:08:57 +0000 | |||
40 | +++ lib/lp/registry/doc/person-merge.txt 2011-03-17 20:19:01 +0000 | |||
41 | @@ -1,10 +1,11 @@ | |||
43 | 1 | = Merging = | 1 | Merging |
44 | 2 | ======= | ||
45 | 2 | 3 | ||
46 | 3 | For many reasons (i.e. a gina run) we could have duplicated accounts in | 4 | For many reasons (i.e. a gina run) we could have duplicated accounts in |
51 | 4 | Launchpad. Once a duplicated account is identified, we need to allow the user | 5 | Launchpad. Once a duplicated account is identified, we need to allow the |
52 | 5 | to merge two accounts into a single one, because both represent the same | 6 | user to merge two accounts into a single one, because both represent the |
53 | 6 | person and they're there just because each of those was created using a | 7 | same person and they're there just because each of those was created |
54 | 7 | different email address. | 8 | using a different email address. |
55 | 8 | 9 | ||
56 | 9 | >>> from zope.component import getUtility | 10 | >>> from zope.component import getUtility |
57 | 10 | >>> from canonical.database.sqlbase import sqlvalues | 11 | >>> from canonical.database.sqlbase import sqlvalues |
58 | @@ -19,7 +20,8 @@ | |||
59 | 19 | >>> marilize = personset.getByName('marilize') | 20 | >>> marilize = personset.getByName('marilize') |
60 | 20 | 21 | ||
61 | 21 | 22 | ||
63 | 22 | == Sanity checks == | 23 | Sanity checks |
64 | 24 | ------------- | ||
65 | 23 | 25 | ||
66 | 24 | We can't merge an account that still has email addresses attached to it | 26 | We can't merge an account that still has email addresses attached to it |
67 | 25 | 27 | ||
68 | @@ -29,13 +31,15 @@ | |||
69 | 29 | AssertionError: ... | 31 | AssertionError: ... |
70 | 30 | 32 | ||
71 | 31 | 33 | ||
73 | 32 | == Preparing test person for the merge == | 34 | Preparing test person for the merge |
74 | 35 | ----------------------------------- | ||
75 | 33 | 36 | ||
76 | 34 | Merging people involves updating the merged person relationships. Let's | 37 | Merging people involves updating the merged person relationships. Let's |
77 | 35 | put the person we will merge into some of those. | 38 | put the person we will merge into some of those. |
78 | 36 | 39 | ||
79 | 37 | # To assign marilize as the ubuntu team owner, we must log on as the | 40 | # To assign marilize as the ubuntu team owner, we must log on as the |
80 | 38 | # previous owner. | 41 | # previous owner. |
81 | 42 | |||
82 | 39 | >>> login('mark@example.com') | 43 | >>> login('mark@example.com') |
83 | 40 | 44 | ||
84 | 41 | >>> ubuntu_team = personset.getByName('ubuntu-team') | 45 | >>> ubuntu_team = personset.getByName('ubuntu-team') |
85 | @@ -55,20 +59,23 @@ | |||
86 | 55 | >>> saved_marilize_karma_id = marilize_karma.id | 59 | >>> saved_marilize_karma_id = marilize_karma.id |
87 | 56 | >>> print marilize_karma.person.name | 60 | >>> print marilize_karma.person.name |
88 | 57 | marilize | 61 | marilize |
89 | 62 | |||
90 | 58 | >>> sampleperson_old_karma = sample.karma | 63 | >>> sampleperson_old_karma = sample.karma |
91 | 59 | 64 | ||
104 | 60 | Branches whose owner is being merged are uniquified by appending '-N' where N | 65 | Branches whose owner is being merged are uniquified by appending '-N' |
105 | 61 | is a unique integer. We create "peoplemerge" and "peoplemerge-1" branches owned | 66 | where N is a unique integer. We create "peoplemerge" and "peoplemerge-1" |
106 | 62 | by marilize, and a "peoplemerge" and "peoplemerge-1" branches owned by 'Sample | 67 | branches owned by marilize, and a "peoplemerge" and "peoplemerge-1" |
107 | 63 | Person' to test that branch name uniquifying works. | 68 | branches owned by 'Sample Person' to test that branch name uniquifying |
108 | 64 | 69 | works. | |
109 | 65 | Branches with smaller IDs will be processed first, so we create "peoplemerge" | 70 | |
110 | 66 | first, and it will be renamed "peoplemerge-2". The extant "peoplemerge-1" | 71 | Branches with smaller IDs will be processed first, so we create |
111 | 67 | branch will be renamed "peoplemerge-1-1". The "peoplemerge-0" branch will not | 72 | "peoplemerge" first, and it will be renamed "peoplemerge-2". The extant |
112 | 68 | be renamed since it will not conflict. | 73 | "peoplemerge-1" branch will be renamed "peoplemerge-1-1". The |
113 | 69 | 74 | "peoplemerge-0" branch will not be renamed since it will not conflict. | |
114 | 70 | That is not a particularly sensible way of renaming branches, but it is simple | 75 | |
115 | 71 | to implement, and it be should extremely rare for the case to occur. | 76 | That is not a particularly sensible way of renaming branches, but it is |
116 | 77 | simple to implement, and it be should extremely rare for the case to | ||
117 | 78 | occur. | ||
118 | 72 | 79 | ||
119 | 73 | >>> peoplemerge = factory.makePersonalBranch( | 80 | >>> peoplemerge = factory.makePersonalBranch( |
120 | 74 | ... name='peoplemerge', owner=sample) | 81 | ... name='peoplemerge', owner=sample) |
121 | @@ -81,12 +88,13 @@ | |||
122 | 81 | >>> peoplemerge11 = factory.makePersonalBranch( | 88 | >>> peoplemerge11 = factory.makePersonalBranch( |
123 | 82 | ... name='peoplemerge-1', owner=marilize) | 89 | ... name='peoplemerge-1', owner=marilize) |
124 | 83 | 90 | ||
128 | 84 | 'Sample Person' is a deactivated member of the 'Ubuntu Translators' team, | 91 | 'Sample Person' is a deactivated member of the 'Ubuntu Translators' |
129 | 85 | while marilize is an active member. After the merge, 'Sample Person' will be an | 92 | team, while marilize is an active member. After the merge, 'Sample |
130 | 86 | active member of that team. | 93 | Person' will be an active member of that team. |
131 | 87 | 94 | ||
132 | 88 | >>> sample in ubuntu_translators.inactivemembers | 95 | >>> sample in ubuntu_translators.inactivemembers |
133 | 89 | True | 96 | True |
134 | 97 | |||
135 | 90 | >>> marilize in ubuntu_translators.activemembers | 98 | >>> marilize in ubuntu_translators.activemembers |
136 | 91 | True | 99 | True |
137 | 92 | 100 | ||
138 | @@ -102,11 +110,13 @@ | |||
139 | 102 | u'Marilize Coetzee' | 110 | u'Marilize Coetzee' |
140 | 103 | 111 | ||
141 | 104 | 112 | ||
143 | 105 | == Do the merge! == | 113 | Do the merge! |
144 | 114 | ------------- | ||
145 | 106 | 115 | ||
146 | 107 | # Now we remove the only email address marilize had, so that we can merge | 116 | # Now we remove the only email address marilize had, so that we can merge |
147 | 108 | # it. First we need to change its status, though, because we can't delete | 117 | # it. First we need to change its status, though, because we can't delete |
148 | 109 | # a person's preferred email. | 118 | # a person's preferred email. |
149 | 119 | |||
150 | 110 | >>> from canonical.launchpad.interfaces.emailaddress import ( | 120 | >>> from canonical.launchpad.interfaces.emailaddress import ( |
151 | 111 | ... EmailAddressStatus) | 121 | ... EmailAddressStatus) |
152 | 112 | >>> from canonical.launchpad.interfaces.lpstorm import IMasterObject | 122 | >>> from canonical.launchpad.interfaces.lpstorm import IMasterObject |
153 | @@ -119,13 +129,15 @@ | |||
154 | 119 | >>> personset.merge(marilize, sample) | 129 | >>> personset.merge(marilize, sample) |
155 | 120 | 130 | ||
156 | 121 | 131 | ||
158 | 122 | == Merge results == | 132 | Merge results |
159 | 133 | ------------- | ||
160 | 123 | 134 | ||
161 | 124 | Check that 'Sample Person' has indeed become an active member of 'Ubuntu | 135 | Check that 'Sample Person' has indeed become an active member of 'Ubuntu |
162 | 125 | Translators' | 136 | Translators' |
163 | 126 | 137 | ||
164 | 127 | >>> sample in ubuntu_translators.activemembers | 138 | >>> sample in ubuntu_translators.activemembers |
165 | 128 | True | 139 | True |
166 | 140 | |||
167 | 129 | >>> sample.inTeam(ubuntu_translators) | 141 | >>> sample.inTeam(ubuntu_translators) |
168 | 130 | True | 142 | True |
169 | 131 | 143 | ||
170 | @@ -136,27 +148,32 @@ | |||
171 | 136 | >>> sample_junk = get_branch_namespace(sample) | 148 | >>> sample_junk = get_branch_namespace(sample) |
172 | 137 | >>> sample_junk.getByName('peoplemerge') == peoplemerge | 149 | >>> sample_junk.getByName('peoplemerge') == peoplemerge |
173 | 138 | True | 150 | True |
174 | 151 | |||
175 | 139 | >>> sample_junk.getByName('peoplemerge-0') == peoplemerge0 | 152 | >>> sample_junk.getByName('peoplemerge-0') == peoplemerge0 |
176 | 140 | True | 153 | True |
177 | 154 | |||
178 | 141 | >>> sample_junk.getByName('peoplemerge-1') == peoplemerge1 | 155 | >>> sample_junk.getByName('peoplemerge-1') == peoplemerge1 |
179 | 142 | True | 156 | True |
180 | 157 | |||
181 | 143 | >>> sample_junk.getByName('peoplemerge-2') == peoplemerge2 | 158 | >>> sample_junk.getByName('peoplemerge-2') == peoplemerge2 |
182 | 144 | True | 159 | True |
183 | 160 | |||
184 | 145 | >>> sample_junk.getByName('peoplemerge-1-1') == peoplemerge11 | 161 | >>> sample_junk.getByName('peoplemerge-1-1') == peoplemerge11 |
185 | 146 | True | 162 | True |
186 | 147 | 163 | ||
189 | 148 | The Karma that was previously assigned to marilize is now assigned to name12 | 164 | The Karma that was previously assigned to marilize is now assigned to |
190 | 149 | (Sample Person). | 165 | name12 (Sample Person). |
191 | 150 | 166 | ||
192 | 151 | >>> from canonical.database.sqlbase import flush_database_caches | 167 | >>> from canonical.database.sqlbase import flush_database_caches |
193 | 152 | >>> flush_database_caches() | 168 | >>> flush_database_caches() |
194 | 153 | >>> saved_marilize_karma_id == marilize_karma.id | 169 | >>> saved_marilize_karma_id == marilize_karma.id |
195 | 154 | True | 170 | True |
196 | 171 | |||
197 | 155 | >>> print marilize_karma.person.name | 172 | >>> print marilize_karma.person.name |
198 | 156 | name12 | 173 | name12 |
199 | 157 | 174 | ||
202 | 158 | Note that we don't bother migrating karma caches - it will just be reset next | 175 | Note that we don't bother migrating karma caches - it will just be reset |
203 | 159 | time the caches are rebuilt. | 176 | next time the caches are rebuilt. |
204 | 160 | 177 | ||
205 | 161 | >>> sample.karma == sampleperson_old_karma | 178 | >>> sample.karma == sampleperson_old_karma |
206 | 162 | True | 179 | True |
207 | @@ -199,8 +216,8 @@ | |||
208 | 199 | >>> results.get_one()[0] is None | 216 | >>> results.get_one()[0] is None |
209 | 200 | True | 217 | True |
210 | 201 | 218 | ||
213 | 202 | An email is sent to the user informing him that he should review his email | 219 | An email is sent to the user informing him that he should review his |
214 | 203 | and mailing list subscription settings. | 220 | email and mailing list subscription settings. |
215 | 204 | 221 | ||
216 | 205 | >>> from lp.registry.interfaces.personnotification import ( | 222 | >>> from lp.registry.interfaces.personnotification import ( |
217 | 206 | ... IPersonNotificationSet) | 223 | ... IPersonNotificationSet) |
218 | @@ -209,11 +226,14 @@ | |||
219 | 209 | >>> notifications = notification_set.getNotificationsToSend() | 226 | >>> notifications = notification_set.getNotificationsToSend() |
220 | 210 | >>> notifications.count() | 227 | >>> notifications.count() |
221 | 211 | 1 | 228 | 1 |
222 | 229 | |||
223 | 212 | >>> notification = notifications[0] | 230 | >>> notification = notifications[0] |
224 | 213 | >>> print notification.person.name | 231 | >>> print notification.person.name |
225 | 214 | name12 | 232 | name12 |
226 | 233 | |||
227 | 215 | >>> print notification.subject | 234 | >>> print notification.subject |
228 | 216 | Launchpad accounts merged | 235 | Launchpad accounts merged |
229 | 236 | |||
230 | 217 | >>> print notification.body | 237 | >>> print notification.body |
231 | 218 | The Launchpad account named 'marilize-merged' was merged into the account | 238 | The Launchpad account named 'marilize-merged' was merged into the account |
232 | 219 | named 'name12'. ... | 239 | named 'name12'. ... |
233 | @@ -226,13 +246,13 @@ | |||
234 | 226 | Person decoration | 246 | Person decoration |
235 | 227 | ----------------- | 247 | ----------------- |
236 | 228 | 248 | ||
240 | 229 | Several tables "extend" the Person table by having additional information | 249 | Several tables "extend" the Person table by having additional |
241 | 230 | that is UNIQUEly keyed to Person.id. We have a utility function that merges | 250 | information that is UNIQUEly keyed to Person.id. We have a utility |
242 | 231 | information in those tables, we test it here. | 251 | function that merges information in those tables, we test it here. |
243 | 232 | 252 | ||
247 | 233 | We will use PersonLocation as an example. There are many permutations and | 253 | We will use PersonLocation as an example. There are many permutations |
248 | 234 | combinations, we will exercise them all, and in each case we'll create, and | 254 | and combinations, we will exercise them all, and in each case we'll |
249 | 235 | then delete, the needed two people. | 255 | create, and then delete, the needed two people. |
250 | 236 | 256 | ||
251 | 237 | >>> from lp.registry.model.person import PersonSet, Person | 257 | >>> from lp.registry.model.person import PersonSet, Person |
252 | 238 | >>> from lp.registry.interfaces.person import PersonCreationRationale | 258 | >>> from lp.registry.interfaces.person import PersonCreationRationale |
253 | @@ -272,11 +292,12 @@ | |||
254 | 272 | >>> winner, loser = endless_supply_of_players.next() | 292 | >>> winner, loser = endless_supply_of_players.next() |
255 | 273 | >>> print decorator_refs(store, winner, loser) | 293 | >>> print decorator_refs(store, winner, loser) |
256 | 274 | <BLANKLINE> | 294 | <BLANKLINE> |
257 | 295 | |||
258 | 275 | >>> personset._merge_person_decoration(winner, loser, skip, | 296 | >>> personset._merge_person_decoration(winner, loser, skip, |
259 | 276 | ... 'PersonLocation', 'person', ['last_modified_by',]) | 297 | ... 'PersonLocation', 'person', ['last_modified_by',]) |
260 | 277 | 298 | ||
263 | 278 | "Skip" should have been updated with the table and unique reference column | 299 | "Skip" should have been updated with the table and unique reference |
264 | 279 | name. | 300 | column name. |
265 | 280 | 301 | ||
266 | 281 | >>> print skip | 302 | >>> print skip |
267 | 282 | [('personlocation', 'person')] | 303 | [('personlocation', 'person')] |
268 | @@ -286,42 +307,44 @@ | |||
269 | 286 | >>> print decorator_refs(store, winner, loser) | 307 | >>> print decorator_refs(store, winner, loser) |
270 | 287 | <BLANKLINE> | 308 | <BLANKLINE> |
271 | 288 | 309 | ||
274 | 289 | OK, now, this time, we will add some decorator information to the winner but | 310 | OK, now, this time, we will add some decorator information to the winner |
275 | 290 | not the loser. | 311 | but not the loser. |
276 | 291 | 312 | ||
277 | 292 | >>> winner, loser = endless_supply_of_players.next() | 313 | >>> winner, loser = endless_supply_of_players.next() |
278 | 293 | >>> winner.setLocation(None, None, 'America/Santiago', winner) | 314 | >>> winner.setLocation(None, None, 'America/Santiago', winner) |
279 | 294 | >>> print decorator_refs(store, winner, loser) | 315 | >>> print decorator_refs(store, winner, loser) |
280 | 295 | winner, winner, | 316 | winner, winner, |
281 | 317 | |||
282 | 296 | >>> personset._merge_person_decoration(winner, loser, skip, | 318 | >>> personset._merge_person_decoration(winner, loser, skip, |
283 | 297 | ... 'PersonLocation', 'person', ['last_modified_by',]) | 319 | ... 'PersonLocation', 'person', ['last_modified_by',]) |
284 | 298 | 320 | ||
287 | 299 | There should now still be one decorator, with all columns pointing to the | 321 | There should now still be one decorator, with all columns pointing to |
288 | 300 | winner: | 322 | the winner: |
289 | 301 | 323 | ||
290 | 302 | >>> print decorator_refs(store, winner, loser) | 324 | >>> print decorator_refs(store, winner, loser) |
291 | 303 | winner, winner, | 325 | winner, winner, |
292 | 304 | 326 | ||
295 | 305 | This time, we will have a decorator for the person that is being merged INTO | 327 | This time, we will have a decorator for the person that is being merged |
296 | 306 | another person, but nothing on the target person. | 328 | INTO another person, but nothing on the target person. |
297 | 307 | 329 | ||
298 | 308 | >>> winner, loser = endless_supply_of_players.next() | 330 | >>> winner, loser = endless_supply_of_players.next() |
299 | 309 | >>> loser.setLocation(None, None, 'America/Santiago', loser) | 331 | >>> loser.setLocation(None, None, 'America/Santiago', loser) |
300 | 310 | >>> print decorator_refs(store, winner, loser) | 332 | >>> print decorator_refs(store, winner, loser) |
301 | 311 | loser, loser, | 333 | loser, loser, |
302 | 334 | |||
303 | 312 | >>> personset._merge_person_decoration(winner, loser, skip, | 335 | >>> personset._merge_person_decoration(winner, loser, skip, |
304 | 313 | ... 'PersonLocation', 'person', ['last_modified_by',]) | 336 | ... 'PersonLocation', 'person', ['last_modified_by',]) |
305 | 314 | 337 | ||
308 | 315 | There should now still be one decorator, with all columns pointing to the | 338 | There should now still be one decorator, with all columns pointing to |
309 | 316 | winner: | 339 | the winner: |
310 | 317 | 340 | ||
311 | 318 | >>> print decorator_refs(store, winner, loser) | 341 | >>> print decorator_refs(store, winner, loser) |
312 | 319 | winner, winner, | 342 | winner, winner, |
313 | 320 | 343 | ||
314 | 321 | Now, we want to show what happens when there is a decorator for both the | 344 | Now, we want to show what happens when there is a decorator for both the |
318 | 322 | to_person and the from_person. We expect that the from_person record will | 345 | to_person and the from_person. We expect that the from_person record |
319 | 323 | remain as noise but non-unique columns will have been updated to point to | 346 | will remain as noise but non-unique columns will have been updated to |
320 | 324 | the winner, and the to_person will be unaffected. | 347 | point to the winner, and the to_person will be unaffected. |
321 | 325 | 348 | ||
322 | 326 | >>> winner, loser = endless_supply_of_players.next() | 349 | >>> winner, loser = endless_supply_of_players.next() |
323 | 327 | >>> winner.setLocation(None, None, 'America/Santiago', winner) | 350 | >>> winner.setLocation(None, None, 'America/Santiago', winner) |
324 | @@ -329,6 +352,7 @@ | |||
325 | 329 | >>> print decorator_refs(store, winner, loser) | 352 | >>> print decorator_refs(store, winner, loser) |
326 | 330 | winner, winner, | 353 | winner, winner, |
327 | 331 | loser, loser, | 354 | loser, loser, |
328 | 355 | |||
329 | 332 | >>> personset._merge_person_decoration(winner, loser, skip, | 356 | >>> personset._merge_person_decoration(winner, loser, skip, |
330 | 333 | ... 'PersonLocation', 'person', ['last_modified_by',]) | 357 | ... 'PersonLocation', 'person', ['last_modified_by',]) |
331 | 334 | >>> print decorator_refs(store, winner, loser) | 358 | >>> print decorator_refs(store, winner, loser) |
332 | @@ -336,12 +360,13 @@ | |||
333 | 336 | loser, winner, | 360 | loser, winner, |
334 | 337 | 361 | ||
335 | 338 | 362 | ||
337 | 339 | == Merging teams == | 363 | Merging teams |
338 | 364 | ------------- | ||
339 | 340 | 365 | ||
340 | 341 | Merging of teams is also possible and uses the same API used for merging | 366 | Merging of teams is also possible and uses the same API used for merging |
341 | 342 | people. Note, though, that when merging teams, its polls will not be | 367 | people. Note, though, that when merging teams, its polls will not be |
344 | 343 | carried over to the remaining team. Team memberships, on the other hand, | 368 | carried over to the remaining team. Team memberships, on the other |
345 | 344 | are carried over just like when merging people. | 369 | hand, are carried over just like when merging people. |
346 | 345 | 370 | ||
347 | 346 | >>> from datetime import datetime, timedelta | 371 | >>> from datetime import datetime, timedelta |
348 | 347 | >>> import pytz | 372 | >>> import pytz |
349 | @@ -357,37 +382,45 @@ | |||
350 | 357 | ... PollSecrecy.OPEN, allowspoilt=True) | 382 | ... PollSecrecy.OPEN, allowspoilt=True) |
351 | 358 | 383 | ||
352 | 359 | # test_team has a superteam, one active member and a poll. | 384 | # test_team has a superteam, one active member and a poll. |
353 | 385 | |||
354 | 360 | >>> [team.name for team in test_team.super_teams] | 386 | >>> [team.name for team in test_team.super_teams] |
355 | 361 | [u'launchpad'] | 387 | [u'launchpad'] |
356 | 388 | |||
357 | 362 | >>> test_team.teamowner.name | 389 | >>> test_team.teamowner.name |
358 | 363 | u'name12' | 390 | u'name12' |
359 | 391 | |||
360 | 364 | >>> [member.name for member in test_team.allmembers] | 392 | >>> [member.name for member in test_team.allmembers] |
361 | 365 | [u'name12'] | 393 | [u'name12'] |
362 | 394 | |||
363 | 366 | >>> list(IPollSubset(test_team).getAll()) | 395 | >>> list(IPollSubset(test_team).getAll()) |
364 | 367 | [<Poll at ...] | 396 | [<Poll at ...] |
365 | 368 | 397 | ||
366 | 369 | # Landscape-developers has no super teams, two members and no polls. | 398 | # Landscape-developers has no super teams, two members and no polls. |
367 | 399 | |||
368 | 370 | >>> landscape = personset.getByName('landscape-developers') | 400 | >>> landscape = personset.getByName('landscape-developers') |
369 | 371 | >>> [team.name for team in landscape.super_teams] | 401 | >>> [team.name for team in landscape.super_teams] |
370 | 372 | [] | 402 | [] |
371 | 403 | |||
372 | 373 | >>> landscape.teamowner.name | 404 | >>> landscape.teamowner.name |
373 | 374 | u'name12' | 405 | u'name12' |
374 | 406 | |||
375 | 375 | >>> [member.name for member in landscape.allmembers] | 407 | >>> [member.name for member in landscape.allmembers] |
376 | 376 | [u'salgado', u'name12'] | 408 | [u'salgado', u'name12'] |
377 | 409 | |||
378 | 377 | >>> list(IPollSubset(landscape).getAll()) | 410 | >>> list(IPollSubset(landscape).getAll()) |
379 | 378 | [] | 411 | [] |
380 | 379 | 412 | ||
383 | 380 | Now we try to merge them, but since test_team has active members it can't be | 413 | Now we try to merge them, but since test_team has active members it |
384 | 381 | merged. | 414 | can't be merged. |
385 | 382 | 415 | ||
386 | 383 | >>> personset.merge(test_team, landscape) | 416 | >>> personset.merge(test_team, landscape) |
387 | 384 | Traceback (most recent call last): | 417 | Traceback (most recent call last): |
388 | 385 | ... | 418 | ... |
389 | 386 | AssertionError: Only teams without active members can be merged | 419 | AssertionError: Only teams without active members can be merged |
390 | 387 | 420 | ||
394 | 388 | A team with a mailing list can only be merged if has no mailing list or if the | 421 | A team with a mailing list can only be merged if has no mailing list or |
395 | 389 | mailing list is in the PURGED state. This state indicates that there are no | 422 | if the mailing list is in the PURGED state. This state indicates that |
396 | 390 | artifacts of the list on the Mailman side. | 423 | there are no artifacts of the list on the Mailman side. |
397 | 391 | 424 | ||
398 | 392 | >>> from lp.registry.interfaces.mailinglist import IMailingListSet | 425 | >>> from lp.registry.interfaces.mailinglist import IMailingListSet |
399 | 393 | >>> mailing_list = getUtility(IMailingListSet).new(landscape) | 426 | >>> mailing_list = getUtility(IMailingListSet).new(landscape) |
400 | @@ -395,21 +428,23 @@ | |||
401 | 395 | >>> mailman.act() | 428 | >>> mailman.act() |
402 | 396 | >>> print mailing_list.status.name | 429 | >>> print mailing_list.status.name |
403 | 397 | ACTIVE | 430 | ACTIVE |
404 | 431 | |||
405 | 398 | >>> ubuntu_team = personset.getByName('ubuntu-team') | 432 | >>> ubuntu_team = personset.getByName('ubuntu-team') |
406 | 399 | >>> personset.merge(landscape, ubuntu_team) | 433 | >>> personset.merge(landscape, ubuntu_team) |
407 | 400 | Traceback (most recent call last): | 434 | Traceback (most recent call last): |
408 | 401 | ... | 435 | ... |
409 | 402 | AssertionError: Can't merge teams which have mailing lists... | 436 | AssertionError: Can't merge teams which have mailing lists... |
410 | 403 | 437 | ||
414 | 404 | Purging the associated mailing list doesn't delete it, but puts it into a | 438 | Purging the associated mailing list doesn't delete it, but puts it into |
415 | 405 | state that essentially treats it as nonexistent. It still can't be merged | 439 | a state that essentially treats it as nonexistent. It still can't be |
416 | 406 | until all the members are deactivated though. | 440 | merged until all the members are deactivated though. |
417 | 407 | 441 | ||
418 | 408 | >>> mailing_list.deactivate() | 442 | >>> mailing_list.deactivate() |
419 | 409 | >>> mailman.act() | 443 | >>> mailman.act() |
420 | 410 | >>> mailing_list.purge() | 444 | >>> mailing_list.purge() |
421 | 411 | >>> print mailing_list.status.name | 445 | >>> print mailing_list.status.name |
422 | 412 | PURGED | 446 | PURGED |
423 | 447 | |||
424 | 413 | >>> personset.merge(landscape, ubuntu_team) | 448 | >>> personset.merge(landscape, ubuntu_team) |
425 | 414 | Traceback (most recent call last): | 449 | Traceback (most recent call last): |
426 | 415 | ... | 450 | ... |
427 | @@ -418,26 +453,34 @@ | |||
428 | 418 | For cases like this we have an API which takes care of deactivating all | 453 | For cases like this we have an API which takes care of deactivating all |
429 | 419 | active members of the team so that we can perform the merge. | 454 | active members of the team so that we can perform the merge. |
430 | 420 | 455 | ||
431 | 456 | >>> from lp.registry.interfaces.teammembership import ITeamMembershipSet | ||
432 | 457 | |||
433 | 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. |
434 | 459 | |||
435 | 422 | >>> login('foo.bar@canonical.com') | 460 | >>> login('foo.bar@canonical.com') |
436 | 423 | 461 | ||
437 | 424 | >>> comment = "We're merging this team into another one." | 462 | >>> comment = "We're merging this team into another one." |
439 | 425 | >>> test_team.deactivateAllMembers(comment, personset.getByName('name16')) | 463 | >>> membershipset = getUtility(ITeamMembershipSet) |
440 | 464 | >>> membershipset.deactivateActiveMemberships( | ||
441 | 465 | ... test_team, comment, personset.getByName('name16')) | ||
442 | 426 | >>> for team in test_team.super_teams: | 466 | >>> for team in test_team.super_teams: |
443 | 427 | ... test_team.retractTeamMembership(team, test_team.teamowner) | 467 | ... test_team.retractTeamMembership(team, test_team.teamowner) |
444 | 428 | 468 | ||
445 | 429 | |||
446 | 430 | >>> personset.merge(test_team, landscape) | 469 | >>> personset.merge(test_team, landscape) |
447 | 431 | 470 | ||
448 | 432 | # The resulting Landscape-developers no new super teams, has | 471 | # The resulting Landscape-developers no new super teams, has |
449 | 433 | # no polls and its members are still the same two from before the | 472 | # no polls and its members are still the same two from before the |
450 | 434 | # merge. | 473 | # merge. |
451 | 474 | |||
452 | 435 | >>> landscape.teamowner.name | 475 | >>> landscape.teamowner.name |
453 | 436 | u'name12' | 476 | u'name12' |
454 | 477 | |||
455 | 437 | >>> [member.name for member in landscape.allmembers] | 478 | >>> [member.name for member in landscape.allmembers] |
456 | 438 | [u'salgado', u'name12'] | 479 | [u'salgado', u'name12'] |
457 | 480 | |||
458 | 439 | >>> [team.name for team in landscape.super_teams] | 481 | >>> [team.name for team in landscape.super_teams] |
459 | 440 | [] | 482 | [] |
460 | 483 | |||
461 | 441 | >>> list(IPollSubset(landscape).getAll()) | 484 | >>> list(IPollSubset(landscape).getAll()) |
462 | 442 | [] | 485 | [] |
463 | 443 | 486 | ||
464 | 444 | 487 | ||
465 | === modified file 'lib/lp/registry/doc/teammembership.txt' | |||
466 | --- lib/lp/registry/doc/teammembership.txt 2011-02-28 23:25:52 +0000 | |||
467 | +++ lib/lp/registry/doc/teammembership.txt 2011-03-17 20:19:01 +0000 | |||
468 | @@ -895,7 +895,8 @@ | |||
469 | 895 | If a team is merged it will not show up in the set of administered teams. | 895 | If a team is merged it will not show up in the set of administered teams. |
470 | 896 | 896 | ||
471 | 897 | >>> login('foo.bar@canonical.com') | 897 | >>> login('foo.bar@canonical.com') |
473 | 898 | >>> cprov_team.deactivateAllMembers("Merging", foobar) | 898 | >>> membershipset.deactivateActiveMemberships( |
474 | 899 | ... cprov_team, "Merging", foobar) | ||
475 | 899 | >>> personset.merge(cprov_team, guadamen_team) | 900 | >>> personset.merge(cprov_team, guadamen_team) |
476 | 900 | >>> [team.name for team in cprov.getAdministratedTeams()] | 901 | >>> [team.name for team in cprov.getAdministratedTeams()] |
477 | 901 | [u'canonical-partner-dev', u'guadamen', u'launchpad-buildd-admins'] | 902 | [u'canonical-partner-dev', u'guadamen', u'launchpad-buildd-admins'] |
478 | 902 | 903 | ||
479 | === modified file 'lib/lp/registry/doc/vocabularies.txt' | |||
480 | --- lib/lp/registry/doc/vocabularies.txt 2010-12-21 21:59:19 +0000 | |||
481 | +++ lib/lp/registry/doc/vocabularies.txt 2011-03-17 20:19:01 +0000 | |||
482 | @@ -1,4 +1,5 @@ | |||
484 | 1 | = Registry vocabularies = | 1 | Registry vocabularies |
485 | 2 | ===================== | ||
486 | 2 | 3 | ||
487 | 3 | >>> from canonical.database.sqlbase import flush_database_updates | 4 | >>> from canonical.database.sqlbase import flush_database_updates |
488 | 4 | >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag | 5 | >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag |
489 | @@ -21,16 +22,18 @@ | |||
490 | 21 | >>> product_vocabulary = get_naked_vocab(None, "Product") | 22 | >>> product_vocabulary = get_naked_vocab(None, "Product") |
491 | 22 | 23 | ||
492 | 23 | 24 | ||
494 | 24 | == ActiveMailingList == | 25 | ActiveMailingList |
495 | 26 | ----------------- | ||
496 | 25 | 27 | ||
499 | 26 | The active mailing lists vocabulary matches and returns only those mailing | 28 | The active mailing lists vocabulary matches and returns only those |
500 | 27 | lists which are active. | 29 | mailing lists which are active. |
501 | 28 | 30 | ||
502 | 29 | >>> list_vocabulary = get_naked_vocab(None, 'ActiveMailingList') | 31 | >>> list_vocabulary = get_naked_vocab(None, 'ActiveMailingList') |
503 | 30 | >>> from canonical.launchpad.webapp.testing import verifyObject | 32 | >>> from canonical.launchpad.webapp.testing import verifyObject |
504 | 31 | >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary | 33 | >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary |
505 | 32 | >>> verifyObject(IHugeVocabulary, list_vocabulary) | 34 | >>> verifyObject(IHugeVocabulary, list_vocabulary) |
506 | 33 | True | 35 | True |
507 | 36 | |||
508 | 34 | >>> list_vocabulary.displayname | 37 | >>> list_vocabulary.displayname |
509 | 35 | 'Select an active mailing list.' | 38 | 'Select an active mailing list.' |
510 | 36 | 39 | ||
511 | @@ -38,17 +41,15 @@ | |||
512 | 38 | 41 | ||
513 | 39 | >>> list(list_vocabulary) | 42 | >>> list(list_vocabulary) |
514 | 40 | [] | 43 | [] |
515 | 44 | |||
516 | 41 | >>> len(list_vocabulary) | 45 | >>> len(list_vocabulary) |
517 | 42 | 0 | 46 | 0 |
518 | 47 | |||
519 | 43 | >>> list(list_vocabulary.search()) | 48 | >>> list(list_vocabulary.search()) |
520 | 44 | [] | 49 | [] |
521 | 45 | 50 | ||
522 | 46 | Mailing lists are not active when they are first registered. | 51 | Mailing lists are not active when they are first registered. |
523 | 47 | 52 | ||
524 | 48 | >>> # These are the convoluted steps to create some mailing lists. We | ||
525 | 49 | >>> # can't use the shortcuts that other related tests use because those | ||
526 | 50 | >>> # leave the list in the ACTIVE state and we want to check things | ||
527 | 51 | >>> # before they get to that state. | ||
528 | 52 | >>> personset = getUtility(IPersonSet) | 53 | >>> personset = getUtility(IPersonSet) |
529 | 53 | >>> ddaa = personset.getByName('ddaa') | 54 | >>> ddaa = personset.getByName('ddaa') |
530 | 54 | >>> carlos = personset.getByName('carlos') | 55 | >>> carlos = personset.getByName('carlos') |
531 | @@ -70,13 +71,15 @@ | |||
532 | 70 | >>> list_three = listset.new(team_three) | 71 | >>> list_three = listset.new(team_three) |
533 | 71 | >>> list(list_vocabulary) | 72 | >>> list(list_vocabulary) |
534 | 72 | [] | 73 | [] |
535 | 74 | |||
536 | 73 | >>> len(list_vocabulary) | 75 | >>> len(list_vocabulary) |
537 | 74 | 0 | 76 | 0 |
538 | 77 | |||
539 | 75 | >>> list(list_vocabulary.search()) | 78 | >>> list(list_vocabulary.search()) |
540 | 76 | [] | 79 | [] |
541 | 77 | 80 | ||
544 | 78 | Mailing lists become active once they have been constructed by Mailman (which | 81 | Mailing lists become active once they have been constructed by Mailman |
545 | 79 | indicates so by transitioning the state to ACTIVE). | 82 | (which indicates so by transitioning the state to ACTIVE). |
546 | 80 | 83 | ||
547 | 81 | >>> list_one.startConstructing() | 84 | >>> list_one.startConstructing() |
548 | 82 | >>> list_two.startConstructing() | 85 | >>> list_two.startConstructing() |
549 | @@ -88,11 +91,12 @@ | |||
550 | 88 | >>> sorted(mailing_list.team.displayname | 91 | >>> sorted(mailing_list.team.displayname |
551 | 89 | ... for mailing_list in list_vocabulary) | 92 | ... for mailing_list in list_vocabulary) |
552 | 90 | [u'Bass Players', u'Drummers', u'Guitar Players'] | 93 | [u'Bass Players', u'Drummers', u'Guitar Players'] |
553 | 94 | |||
554 | 91 | >>> len(list_vocabulary) | 95 | >>> len(list_vocabulary) |
555 | 92 | 3 | 96 | 3 |
556 | 93 | 97 | ||
559 | 94 | Searching for active lists is done through the vocabulary as well. With a | 98 | Searching for active lists is done through the vocabulary as well. With |
560 | 95 | search term of None, all active lists are returned. | 99 | a search term of None, all active lists are returned. |
561 | 96 | 100 | ||
562 | 97 | >>> sorted(mailing_list.team.displayname | 101 | >>> sorted(mailing_list.team.displayname |
563 | 98 | ... for mailing_list in list_vocabulary.search(None)) | 102 | ... for mailing_list in list_vocabulary.search(None)) |
564 | @@ -104,27 +108,31 @@ | |||
565 | 104 | ... for mailing_list in list_vocabulary.search('player')) | 108 | ... for mailing_list in list_vocabulary.search('player')) |
566 | 105 | [u'Bass Players', u'Guitar Players'] | 109 | [u'Bass Players', u'Guitar Players'] |
567 | 106 | 110 | ||
570 | 107 | The IHugeVocabulary interface also requires a search method that returns a | 111 | The IHugeVocabulary interface also requires a search method that returns |
571 | 108 | CountableIterator. | 112 | a CountableIterator. |
572 | 109 | 113 | ||
573 | 110 | >>> iter = list_vocabulary.searchForTerms('player') | 114 | >>> iter = list_vocabulary.searchForTerms('player') |
574 | 111 | >>> from canonical.launchpad.webapp.vocabulary import CountableIterator | 115 | >>> from canonical.launchpad.webapp.vocabulary import CountableIterator |
575 | 112 | >>> isinstance(iter, CountableIterator) | 116 | >>> isinstance(iter, CountableIterator) |
576 | 113 | True | 117 | True |
577 | 118 | |||
578 | 114 | >>> sorted((term.value.team.name, term.token, term.title) | 119 | >>> sorted((term.value.team.name, term.token, term.title) |
579 | 115 | ... for term in iter) | 120 | ... for term in iter) |
580 | 116 | [(u'bass-players', 'bass-players', u'Bass Players'), | 121 | [(u'bass-players', 'bass-players', u'Bass Players'), |
581 | 117 | (u'guitar-players', 'guitar-players', u'Guitar Players')] | 122 | (u'guitar-players', 'guitar-players', u'Guitar Players')] |
582 | 118 | 123 | ||
586 | 119 | The vocabulary supports accessing mailing lists by 'term', where the term must | 124 | The vocabulary supports accessing mailing lists by 'term', where the |
587 | 120 | be a mailing list. The returned term's value is the mailing list object, the | 125 | term must be a mailing list. The returned term's value is the mailing |
588 | 121 | token is the team name and the title is the team's display name. | 126 | list object, the token is the team name and the title is the team's |
589 | 127 | display name. | ||
590 | 122 | 128 | ||
591 | 123 | >>> term_1 = list_vocabulary.getTerm(list_two) | 129 | >>> term_1 = list_vocabulary.getTerm(list_two) |
592 | 124 | >>> term_1.value.team.displayname | 130 | >>> term_1.value.team.displayname |
593 | 125 | u'Guitar Players' | 131 | u'Guitar Players' |
594 | 132 | |||
595 | 126 | >>> term_1.token | 133 | >>> term_1.token |
596 | 127 | 'guitar-players' | 134 | 'guitar-players' |
597 | 135 | |||
598 | 128 | >>> term_1.title | 136 | >>> term_1.title |
599 | 129 | u'Guitar Players' | 137 | u'Guitar Players' |
600 | 130 | 138 | ||
601 | @@ -140,13 +148,14 @@ | |||
602 | 140 | >>> term_2 = list_vocabulary.getTermByToken(term_1.token) | 148 | >>> term_2 = list_vocabulary.getTermByToken(term_1.token) |
603 | 141 | >>> term_2.value.team.displayname | 149 | >>> term_2.value.team.displayname |
604 | 142 | u'Guitar Players' | 150 | u'Guitar Players' |
605 | 151 | |||
606 | 143 | >>> term_3 = list_vocabulary.getTerm(list_one) | 152 | >>> term_3 = list_vocabulary.getTerm(list_one) |
607 | 144 | >>> term_4 = list_vocabulary.getTermByToken(term_3.token) | 153 | >>> term_4 = list_vocabulary.getTermByToken(term_3.token) |
608 | 145 | >>> term_4.value.team.displayname | 154 | >>> term_4.value.team.displayname |
609 | 146 | u'Bass Players' | 155 | u'Bass Players' |
610 | 147 | 156 | ||
613 | 148 | If you try to get the term by a token not represented in the vocabulary, you | 157 | If you try to get the term by a token not represented in the vocabulary, |
614 | 149 | get an exception. | 158 | you get an exception. |
615 | 150 | 159 | ||
616 | 151 | >>> list_vocabulary.getTermByToken('turntablists') | 160 | >>> list_vocabulary.getTermByToken('turntablists') |
617 | 152 | Traceback (most recent call last): | 161 | Traceback (most recent call last): |
618 | @@ -158,8 +167,8 @@ | |||
619 | 158 | >>> list_three in list_vocabulary | 167 | >>> list_three in list_vocabulary |
620 | 159 | True | 168 | True |
621 | 160 | 169 | ||
624 | 161 | You are not allowed to ask whether a non-mailing list object is contained in | 170 | You are not allowed to ask whether a non-mailing list object is |
625 | 162 | this vocabulary. | 171 | contained in this vocabulary. |
626 | 163 | 172 | ||
627 | 164 | >>> team_three in list_vocabulary | 173 | >>> team_three in list_vocabulary |
628 | 165 | Traceback (most recent call last): | 174 | Traceback (most recent call last): |
629 | @@ -179,11 +188,13 @@ | |||
630 | 179 | 188 | ||
631 | 180 | >>> list(list_vocabulary.search('flautists')) | 189 | >>> list(list_vocabulary.search('flautists')) |
632 | 181 | [] | 190 | [] |
633 | 191 | |||
634 | 182 | >>> list(list_vocabulary.search('cellists')) | 192 | >>> list(list_vocabulary.search('cellists')) |
635 | 183 | [] | 193 | [] |
636 | 184 | 194 | ||
637 | 185 | 195 | ||
639 | 186 | === DistroSeriesVocabulary === | 196 | DistroSeriesVocabulary |
640 | 197 | ...................... | ||
641 | 187 | 198 | ||
642 | 188 | Reflects the available distribution series. Results are ordered by | 199 | Reflects the available distribution series. Results are ordered by |
643 | 189 | `name` | 200 | `name` |
644 | @@ -227,7 +238,8 @@ | |||
645 | 227 | [] | 238 | [] |
646 | 228 | 239 | ||
647 | 229 | 240 | ||
649 | 230 | === PersonActiveMembership === | 241 | PersonActiveMembership |
650 | 242 | ...................... | ||
651 | 231 | 243 | ||
652 | 232 | All the teams the person is an active member of. | 244 | All the teams the person is an active member of. |
653 | 233 | 245 | ||
654 | @@ -236,6 +248,7 @@ | |||
655 | 236 | ... foo_bar, 'PersonActiveMembership') | 248 | ... foo_bar, 'PersonActiveMembership') |
656 | 237 | >>> len(person_active_membership) | 249 | >>> len(person_active_membership) |
657 | 238 | 10 | 250 | 10 |
658 | 251 | |||
659 | 239 | >>> for term in person_active_membership: | 252 | >>> for term in person_active_membership: |
660 | 240 | ... print term.token, term.value.displayname, term.title | 253 | ... print term.token, term.value.displayname, term.title |
661 | 241 | canonical-partner-dev Canonical Partner Developers | 254 | canonical-partner-dev Canonical Partner Developers |
662 | @@ -253,6 +266,7 @@ | |||
663 | 253 | >>> launchpad_team = person_set.getByName('launchpad') | 266 | >>> launchpad_team = person_set.getByName('launchpad') |
664 | 254 | >>> launchpad_team in person_active_membership | 267 | >>> launchpad_team in person_active_membership |
665 | 255 | True | 268 | True |
666 | 269 | |||
667 | 256 | >>> mirrors_admins = person_set.getByName('mirrors-admins') | 270 | >>> mirrors_admins = person_set.getByName('mirrors-admins') |
668 | 257 | >>> mirrors_admins in person_active_membership | 271 | >>> mirrors_admins in person_active_membership |
669 | 258 | False | 272 | False |
670 | @@ -313,7 +327,8 @@ | |||
671 | 313 | LookupError:... | 327 | LookupError:... |
672 | 314 | 328 | ||
673 | 315 | 329 | ||
675 | 316 | === PersonTeamParticipations === | 330 | PersonTeamParticipations |
676 | 331 | ........................ | ||
677 | 317 | 332 | ||
678 | 318 | This vocabulary contains all the teams a person participates in. Either | 333 | This vocabulary contains all the teams a person participates in. Either |
679 | 319 | through direct or indirect participations. | 334 | through direct or indirect participations. |
680 | @@ -322,6 +337,7 @@ | |||
681 | 322 | >>> [membership.team.name | 337 | >>> [membership.team.name |
682 | 323 | ... for membership in sample_person.team_memberships] | 338 | ... for membership in sample_person.team_memberships] |
683 | 324 | [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20'] | 339 | [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20'] |
684 | 340 | |||
685 | 325 | >>> [team.name for team in sample_person.teams_participated_in] | 341 | >>> [team.name for team in sample_person.teams_participated_in] |
686 | 326 | [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18', | 342 | [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18', |
687 | 327 | u'name20'] | 343 | u'name20'] |
688 | @@ -338,16 +354,17 @@ | |||
689 | 338 | name20: Warty Security Team (name20) | 354 | name20: Warty Security Team (name20) |
690 | 339 | 355 | ||
691 | 340 | 356 | ||
693 | 341 | === Milestone === | 357 | Milestone |
694 | 358 | ......... | ||
695 | 342 | 359 | ||
696 | 343 | All the milestone in a context. | 360 | All the milestone in a context. |
697 | 344 | 361 | ||
698 | 345 | A MilestoneVolcabulary contains different milestones, depending on the | 362 | A MilestoneVolcabulary contains different milestones, depending on the |
699 | 346 | current context. It is pointless to present the large number of all | 363 | current context. It is pointless to present the large number of all |
700 | 347 | active milestones known in Launchpad in a vocabulary. Hence a | 364 | active milestones known in Launchpad in a vocabulary. Hence a |
704 | 348 | MilestoneVolcabulary contains only those milestones that are related | 365 | MilestoneVolcabulary contains only those milestones that are related to |
705 | 349 | to the current context. If no context is given, or if the context does | 366 | the current context. If no context is given, or if the context does not |
706 | 350 | not have any milestones, a MilestoneVocabulary is empty... | 367 | have any milestones, a MilestoneVocabulary is empty... |
707 | 351 | 368 | ||
708 | 352 | >>> milestones = get_naked_vocab(None, 'Milestone') | 369 | >>> milestones = get_naked_vocab(None, 'Milestone') |
709 | 353 | >>> len(milestones) | 370 | >>> len(milestones) |
710 | @@ -359,14 +376,15 @@ | |||
711 | 359 | >>> len(milestones) | 376 | >>> len(milestones) |
712 | 360 | 0 | 377 | 0 |
713 | 361 | 378 | ||
717 | 362 | ...but if the context is an IPerson, the MilestoneVocabulary contains all | 379 | ...but if the context is an IPerson, the MilestoneVocabulary contains |
718 | 363 | milestones. IPerson related pages showing milestone lists retrieve the | 380 | all milestones. IPerson related pages showing milestone lists retrieve |
719 | 364 | milestones from RelevantMilestonesMixin.getMilestoneWidgetValues() | 381 | the milestones from RelevantMilestonesMixin.getMilestoneWidgetValues() |
720 | 365 | but we need the big default vocabulary for form input validation. | 382 | but we need the big default vocabulary for form input validation. |
721 | 366 | 383 | ||
722 | 367 | >>> all_milestones = get_naked_vocab(sample_person, 'Milestone') | 384 | >>> all_milestones = get_naked_vocab(sample_person, 'Milestone') |
723 | 368 | >>> len(all_milestones) | 385 | >>> len(all_milestones) |
724 | 369 | 3 | 386 | 3 |
725 | 387 | |||
726 | 370 | >>> for term in all_milestones: | 388 | >>> for term in all_milestones: |
727 | 371 | ... print "%s: %s" % (term.value.target.name, term.value.name) | 389 | ... print "%s: %s" % (term.value.target.name, term.value.name) |
728 | 372 | debian: 3.1 | 390 | debian: 3.1 |
729 | @@ -403,6 +421,7 @@ | |||
730 | 403 | >>> firefox_task = bug_one.bugtasks[0] | 421 | >>> firefox_task = bug_one.bugtasks[0] |
731 | 404 | >>> firefox_task.bugtargetdisplayname | 422 | >>> firefox_task.bugtargetdisplayname |
732 | 405 | u'Mozilla Firefox' | 423 | u'Mozilla Firefox' |
733 | 424 | |||
734 | 406 | >>> firefox_task_milestones = get_naked_vocab( | 425 | >>> firefox_task_milestones = get_naked_vocab( |
735 | 407 | ... firefox_task, 'Milestone') | 426 | ... firefox_task, 'Milestone') |
736 | 408 | >>> for term in firefox_task_milestones: | 427 | >>> for term in firefox_task_milestones: |
737 | @@ -414,14 +433,15 @@ | |||
738 | 414 | >>> debian_woody_task = bug_two.bugtasks[-1] | 433 | >>> debian_woody_task = bug_two.bugtasks[-1] |
739 | 415 | >>> debian_woody_task.bugtargetdisplayname | 434 | >>> debian_woody_task.bugtargetdisplayname |
740 | 416 | u'mozilla-firefox (Debian Woody)' | 435 | u'mozilla-firefox (Debian Woody)' |
741 | 436 | |||
742 | 417 | >>> debian_woody_milestones = get_naked_vocab( | 437 | >>> debian_woody_milestones = get_naked_vocab( |
743 | 418 | ... debian_woody_task, 'Milestone') | 438 | ... debian_woody_task, 'Milestone') |
744 | 419 | >>> debian_woody = debian_woody_task.distroseries | 439 | >>> debian_woody = debian_woody_task.distroseries |
745 | 420 | >>> len(debian_woody_milestones) | 440 | >>> len(debian_woody_milestones) |
746 | 421 | 2 | 441 | 2 |
747 | 422 | 442 | ||
750 | 423 | If one of the milestones is disabled, it won't be included in the vocabulary | 443 | If one of the milestones is disabled, it won't be included in the |
751 | 424 | anymore. | 444 | vocabulary anymore. |
752 | 425 | 445 | ||
753 | 426 | >>> milestone = debian_woody.milestones[0] | 446 | >>> milestone = debian_woody.milestones[0] |
754 | 427 | >>> milestone.active = False | 447 | >>> milestone.active = False |
755 | @@ -429,8 +449,9 @@ | |||
756 | 429 | >>> len(get_naked_vocab(debian_woody_task, 'Milestone')) | 449 | >>> len(get_naked_vocab(debian_woody_task, 'Milestone')) |
757 | 430 | 1 | 450 | 1 |
758 | 431 | 451 | ||
761 | 432 | If the milestone was used in a bugtask before it was marked inactive, though, | 452 | If the milestone was used in a bugtask before it was marked inactive, |
762 | 433 | it'll still show up on the vocabulary so that users can change it. | 453 | though, it'll still show up on the vocabulary so that users can change |
763 | 454 | it. | ||
764 | 434 | 455 | ||
765 | 435 | >>> debian_woody_task.milestone = milestone | 456 | >>> debian_woody_task.milestone = milestone |
766 | 436 | >>> flush_database_updates() | 457 | >>> flush_database_updates() |
767 | @@ -466,8 +487,8 @@ | |||
768 | 466 | firefox: 1.0 | 487 | firefox: 1.0 |
769 | 467 | firefox: firefox-milestone-no-series | 488 | firefox: firefox-milestone-no-series |
770 | 468 | 489 | ||
773 | 469 | If the context is a specification, only milestones from that specification | 490 | If the context is a specification, only milestones from that |
774 | 470 | target are in the vocabulary. | 491 | specification target are in the vocabulary. |
775 | 471 | 492 | ||
776 | 472 | >>> canvas_spec = firefox.getSpecification('canvas') | 493 | >>> canvas_spec = firefox.getSpecification('canvas') |
777 | 473 | >>> spec_target_milestones = get_naked_vocab( | 494 | >>> spec_target_milestones = get_naked_vocab( |
778 | @@ -485,18 +506,21 @@ | |||
779 | 485 | >>> one_dot_o = firefox.milestones[0] | 506 | >>> one_dot_o = firefox.milestones[0] |
780 | 486 | >>> one_dot_o.name | 507 | >>> one_dot_o.name |
781 | 487 | u'1.0' | 508 | u'1.0' |
782 | 509 | |||
783 | 488 | >>> one_dot_o.active = False | 510 | >>> one_dot_o.active = False |
784 | 489 | 511 | ||
785 | 490 | >>> firefox_milestones = get_naked_vocab(firefox, 'Milestone') | 512 | >>> firefox_milestones = get_naked_vocab(firefox, 'Milestone') |
786 | 491 | >>> len(firefox_milestones) | 513 | >>> len(firefox_milestones) |
787 | 492 | 0 | 514 | 0 |
788 | 515 | |||
789 | 493 | >>> firefox_task_milestones = get_naked_vocab( | 516 | >>> firefox_task_milestones = get_naked_vocab( |
790 | 494 | ... firefox_task, 'Milestone') | 517 | ... firefox_task, 'Milestone') |
791 | 495 | >>> len(firefox_task_milestones) | 518 | >>> len(firefox_task_milestones) |
792 | 496 | 0 | 519 | 0 |
793 | 497 | 520 | ||
794 | 498 | 521 | ||
796 | 499 | === ProjectProductsVocabulary === | 522 | ProjectProductsVocabulary |
797 | 523 | ......................... | ||
798 | 500 | 524 | ||
799 | 501 | All the products in a project. | 525 | All the products in a project. |
800 | 502 | 526 | ||
801 | @@ -510,7 +534,8 @@ | |||
802 | 510 | thunderbird: Mozilla Thunderbird | 534 | thunderbird: Mozilla Thunderbird |
803 | 511 | 535 | ||
804 | 512 | 536 | ||
806 | 513 | === ProjectGroupVocabulary === | 537 | ProjectGroupVocabulary |
807 | 538 | ...................... | ||
808 | 514 | 539 | ||
809 | 515 | The list of selectable projects. The results are ordered by displayname. | 540 | The list of selectable projects. The results are ordered by displayname. |
810 | 516 | 541 | ||
811 | @@ -520,6 +545,7 @@ | |||
812 | 520 | 545 | ||
813 | 521 | >>> [p.title for p in project_vocabulary.search('mozilla')] | 546 | >>> [p.title for p in project_vocabulary.search('mozilla')] |
814 | 522 | [u'The Mozilla Project'] | 547 | [u'The Mozilla Project'] |
815 | 548 | |||
816 | 523 | >>> mozilla = project_vocabulary.getTermByToken('mozilla') | 549 | >>> mozilla = project_vocabulary.getTermByToken('mozilla') |
817 | 524 | >>> mozilla.title | 550 | >>> mozilla.title |
818 | 525 | u'The Mozilla Project' | 551 | u'The Mozilla Project' |
819 | @@ -533,6 +559,7 @@ | |||
820 | 533 | 559 | ||
821 | 534 | >>> [p.title for p in project_vocabulary.search('mozilla')] | 560 | >>> [p.title for p in project_vocabulary.search('mozilla')] |
822 | 535 | [u'The Mozilla Project'] | 561 | [u'The Mozilla Project'] |
823 | 562 | |||
824 | 536 | >>> moz_project.active = False | 563 | >>> moz_project.active = False |
825 | 537 | >>> flush_database_updates() | 564 | >>> flush_database_updates() |
826 | 538 | >>> moz_project in project_vocabulary | 565 | >>> moz_project in project_vocabulary |
827 | @@ -540,11 +567,13 @@ | |||
828 | 540 | 567 | ||
829 | 541 | >>> [p.title for p in project_vocabulary.search('mozilla')] | 568 | >>> [p.title for p in project_vocabulary.search('mozilla')] |
830 | 542 | [] | 569 | [] |
831 | 570 | |||
832 | 543 | >>> moz_project.active = True | 571 | >>> moz_project.active = True |
833 | 544 | >>> flush_database_updates() | 572 | >>> flush_database_updates() |
834 | 545 | 573 | ||
835 | 546 | 574 | ||
837 | 547 | === ProductReleaseVocabulary === | 575 | ProductReleaseVocabulary |
838 | 576 | ........................ | ||
839 | 548 | 577 | ||
840 | 549 | The list of selectable products releases. | 578 | The list of selectable products releases. |
841 | 550 | 579 | ||
842 | @@ -554,6 +583,7 @@ | |||
843 | 554 | 583 | ||
844 | 555 | >>> list(productrelease_vocabulary.search(None)) | 584 | >>> list(productrelease_vocabulary.search(None)) |
845 | 556 | [] | 585 | [] |
846 | 586 | |||
847 | 557 | >>> evolution_releases = productrelease_vocabulary.search("evolution") | 587 | >>> evolution_releases = productrelease_vocabulary.search("evolution") |
848 | 558 | >>> l = [release_term.title for release_term in evolution_releases] | 588 | >>> l = [release_term.title for release_term in evolution_releases] |
849 | 559 | >>> release = productrelease_vocabulary.getTermByToken( | 589 | >>> release = productrelease_vocabulary.getTermByToken( |
850 | @@ -562,10 +592,11 @@ | |||
851 | 562 | u'evolution trunk 2.1.6' | 592 | u'evolution trunk 2.1.6' |
852 | 563 | 593 | ||
853 | 564 | 594 | ||
855 | 565 | === PersonAccountToMergeVocabulary === | 595 | PersonAccountToMergeVocabulary |
856 | 596 | .............................. | ||
857 | 566 | 597 | ||
860 | 567 | All non-merged people with at least one email address. This vocabulary is | 598 | All non-merged people with at least one email address. This vocabulary |
861 | 568 | meant to be used only in the people merge form. | 599 | is meant to be used only in the people merge form. |
862 | 569 | 600 | ||
863 | 570 | >>> vocab = get_naked_vocab(None, "PersonAccountToMerge") | 601 | >>> vocab = get_naked_vocab(None, "PersonAccountToMerge") |
864 | 571 | >>> vocab.displayname | 602 | >>> vocab.displayname |
865 | @@ -576,8 +607,8 @@ | |||
866 | 576 | >>> list(vocab.search(None)) | 607 | >>> list(vocab.search(None)) |
867 | 577 | [] | 608 | [] |
868 | 578 | 609 | ||
871 | 579 | Searching for 'Launchpad Administrators' will return an empty list, because | 610 | Searching for 'Launchpad Administrators' will return an empty list, |
872 | 580 | teams are not part of this vocabulary. | 611 | because teams are not part of this vocabulary. |
873 | 581 | 612 | ||
874 | 582 | >>> [item.name for item in list(vocab.search('Launchpad Administrators'))] | 613 | >>> [item.name for item in list(vocab.search('Launchpad Administrators'))] |
875 | 583 | [] | 614 | [] |
876 | @@ -632,6 +663,7 @@ | |||
877 | 632 | True | 663 | True |
878 | 633 | 664 | ||
879 | 634 | # Here we cheat because IPerson.merged is a readonly attribute. | 665 | # Here we cheat because IPerson.merged is a readonly attribute. |
880 | 666 | |||
881 | 635 | >>> naked_cprov = removeSecurityProxy(cprov) | 667 | >>> naked_cprov = removeSecurityProxy(cprov) |
882 | 636 | >>> naked_cprov.merged = 1 | 668 | >>> naked_cprov.merged = 1 |
883 | 637 | >>> naked_cprov.syncUpdate() | 669 | >>> naked_cprov.syncUpdate() |
884 | @@ -653,7 +685,8 @@ | |||
885 | 653 | True | 685 | True |
886 | 654 | 686 | ||
887 | 655 | 687 | ||
889 | 656 | == AdminMergeablePerson == | 688 | AdminMergeablePerson |
890 | 689 | -------------------- | ||
891 | 657 | 690 | ||
892 | 658 | The set of non-merged people. | 691 | The set of non-merged people. |
893 | 659 | 692 | ||
894 | @@ -661,18 +694,21 @@ | |||
895 | 661 | >>> vocab.displayname | 694 | >>> vocab.displayname |
896 | 662 | 'Select a Person to Merge' | 695 | 'Select a Person to Merge' |
897 | 663 | 696 | ||
900 | 664 | Unlike PersonAccountToMerge, this vocabulary includes people who don't have a | 697 | Unlike PersonAccountToMerge, this vocabulary includes people who don't |
901 | 665 | single email address, as it's fine for admins to merge them. | 698 | have a single email address, as it's fine for admins to merge them. |
902 | 666 | 699 | ||
903 | 667 | >>> print fooperson.preferredemail | 700 | >>> print fooperson.preferredemail |
904 | 668 | None | 701 | None |
905 | 702 | |||
906 | 669 | >>> list(fooperson.validatedemails) + list(fooperson.guessedemails) | 703 | >>> list(fooperson.validatedemails) + list(fooperson.guessedemails) |
907 | 670 | [] | 704 | [] |
908 | 705 | |||
909 | 671 | >>> fooperson in vocab | 706 | >>> fooperson in vocab |
910 | 672 | True | 707 | True |
911 | 673 | 708 | ||
912 | 674 | 709 | ||
914 | 675 | === NonMergedPeopleAndTeams === | 710 | NonMergedPeopleAndTeams |
915 | 711 | ....................... | ||
916 | 676 | 712 | ||
917 | 677 | All non-merged people and teams. | 713 | All non-merged people and teams. |
918 | 678 | 714 | ||
919 | @@ -683,8 +719,8 @@ | |||
920 | 683 | >>> list(vocab.search(None)) | 719 | >>> list(vocab.search(None)) |
921 | 684 | [] | 720 | [] |
922 | 685 | 721 | ||
925 | 686 | This vocabulary includes both validated and unvalidated profiles, as well | 722 | This vocabulary includes both validated and unvalidated profiles, as |
926 | 687 | as teams: | 723 | well as teams: |
927 | 688 | 724 | ||
928 | 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')] |
929 | 690 | [(u'matsubara', False)] | 726 | [(u'matsubara', False)] |
930 | @@ -704,12 +740,13 @@ | |||
931 | 704 | False | 740 | False |
932 | 705 | 741 | ||
933 | 706 | 742 | ||
935 | 707 | === ValidPersonOrTeam === | 743 | ValidPersonOrTeam |
936 | 744 | ................. | ||
937 | 708 | 745 | ||
938 | 709 | All 'valid' persons or teams. This is currently defined as people with a | 746 | All 'valid' persons or teams. This is currently defined as people with a |
939 | 710 | password, a preferred email address and not merged (Person.merged is | 747 | password, a preferred email address and not merged (Person.merged is |
942 | 711 | None). It also includes all public teams and private teams the | 748 | None). It also includes all public teams and private teams the user has |
943 | 712 | user has permission to view. | 749 | permission to view. |
944 | 713 | 750 | ||
945 | 714 | >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam") | 751 | >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam") |
946 | 715 | >>> vocab.displayname | 752 | >>> vocab.displayname |
947 | @@ -723,11 +760,12 @@ | |||
948 | 723 | 760 | ||
949 | 724 | >>> vocab.getTermByToken('name16').value.displayname | 761 | >>> vocab.getTermByToken('name16').value.displayname |
950 | 725 | u'Foo Bar' | 762 | u'Foo Bar' |
951 | 763 | |||
952 | 726 | >>> vocab.getTermByToken('foo.bar@canonical.com').value.displayname | 764 | >>> vocab.getTermByToken('foo.bar@canonical.com').value.displayname |
953 | 727 | u'Foo Bar' | 765 | u'Foo Bar' |
954 | 728 | 766 | ||
957 | 729 | Almost all teams have the word 'team' as part of their names, so a search | 767 | Almost all teams have the word 'team' as part of their names, so a |
958 | 730 | for 'team' should give us some of them. Notice that the | 768 | search for 'team' should give us some of them. Notice that the |
959 | 731 | PRIVATE_TEAM 'myteam' is not included in the results. | 769 | PRIVATE_TEAM 'myteam' is not included in the results. |
960 | 732 | 770 | ||
961 | 733 | >>> login_person(sample_person) | 771 | >>> login_person(sample_person) |
962 | @@ -740,8 +778,11 @@ | |||
963 | 740 | 778 | ||
964 | 741 | Valid teams do not include teams that have been merged. | 779 | Valid teams do not include teams that have been merged. |
965 | 742 | 780 | ||
966 | 781 | >>> from lp.registry.interfaces.teammembership import ITeamMembershipSet | ||
967 | 743 | >>> login_person(foo_bar) | 782 | >>> login_person(foo_bar) |
969 | 744 | >>> ephemeral.deactivateAllMembers("Merging", foo_bar) | 783 | >>> membershipset = getUtility(ITeamMembershipSet) |
970 | 784 | >>> membershipset.deactivateActiveMemberships( | ||
971 | 785 | ... ephemeral, "Merging", foo_bar) | ||
972 | 745 | >>> person_set.merge(ephemeral, foo_bar) | 786 | >>> person_set.merge(ephemeral, foo_bar) |
973 | 746 | >>> login_person(sample_person) | 787 | >>> login_person(sample_person) |
974 | 747 | >>> sorted(person.name for person in vocab.search('team')) | 788 | >>> sorted(person.name for person in vocab.search('team')) |
975 | @@ -750,7 +791,8 @@ | |||
976 | 750 | u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team', | 791 | u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team', |
977 | 751 | u'warty-gnome'] | 792 | u'warty-gnome'] |
978 | 752 | 793 | ||
980 | 753 | A PRIVATE team is displayed when the logged in user is a member of the team. | 794 | A PRIVATE team is displayed when the logged in user is a member of the |
981 | 795 | team. | ||
982 | 754 | 796 | ||
983 | 755 | >>> commercial = person_set.getByEmail('commercial-member@canonical.com') | 797 | >>> commercial = person_set.getByEmail('commercial-member@canonical.com') |
984 | 756 | >>> vocab = get_naked_vocab(commercial, "ValidPersonOrTeam") | 798 | >>> vocab = get_naked_vocab(commercial, "ValidPersonOrTeam") |
985 | @@ -776,7 +818,8 @@ | |||
986 | 776 | u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team', | 818 | u'testing-spanish-team', u'ubuntu-security', u'ubuntu-team', |
987 | 777 | u'warty-gnome'] | 819 | u'warty-gnome'] |
988 | 778 | 820 | ||
990 | 779 | The PRIVATE team can be looked up via getTermByToken for a member of the team. | 821 | The PRIVATE team can be looked up via getTermByToken for a member of the |
991 | 822 | team. | ||
992 | 780 | 823 | ||
993 | 781 | >>> term = vocab.getTermByToken('private-team') | 824 | >>> term = vocab.getTermByToken('private-team') |
994 | 782 | >>> print term.title | 825 | >>> print term.title |
995 | @@ -818,15 +861,16 @@ | |||
996 | 818 | [...u'private-team'...] | 861 | [...u'private-team'...] |
997 | 819 | 862 | ||
998 | 820 | A search for 'support' will give us only the persons which have support | 863 | A search for 'support' will give us only the persons which have support |
1001 | 821 | as part of their name or displayname, or the beginning of | 864 | as part of their name or displayname, or the beginning of one of its |
1002 | 822 | one of its email addresses. | 865 | email addresses. |
1003 | 823 | 866 | ||
1004 | 824 | >>> login('foo.bar@canonical.com') | 867 | >>> login('foo.bar@canonical.com') |
1005 | 825 | >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam") | 868 | >>> vocab = get_naked_vocab(None, "ValidPersonOrTeam") |
1006 | 826 | >>> sorted(person.name for person in vocab.search('support')) | 869 | >>> sorted(person.name for person in vocab.search('support')) |
1007 | 827 | [u'ubuntu-team'] | 870 | [u'ubuntu-team'] |
1008 | 828 | 871 | ||
1010 | 829 | Matsubara doesn't have a preferred email address; he's not a valid Person. | 872 | Matsubara doesn't have a preferred email address; he's not a valid |
1011 | 873 | Person. | ||
1012 | 830 | 874 | ||
1013 | 831 | >>> sorted(person.name for person in vocab.search('matsubara')) | 875 | >>> sorted(person.name for person in vocab.search('matsubara')) |
1014 | 832 | [] | 876 | [] |
1015 | @@ -841,13 +885,14 @@ | |||
1016 | 841 | >>> [cjwatson] = vocab.search('cjwatson') | 885 | >>> [cjwatson] = vocab.search('cjwatson') |
1017 | 842 | >>> cjwatson.name, cjwatson.preferredemail.email | 886 | >>> cjwatson.name, cjwatson.preferredemail.email |
1018 | 843 | (u'kamion', u'colin.watson@ubuntulinux.com') | 887 | (u'kamion', u'colin.watson@ubuntulinux.com') |
1019 | 888 | |||
1020 | 844 | >>> [ircid.nickname for ircid in cjwatson.ircnicknames] | 889 | >>> [ircid.nickname for ircid in cjwatson.ircnicknames] |
1021 | 845 | [u'cjwatson'] | 890 | [u'cjwatson'] |
1022 | 846 | 891 | ||
1027 | 847 | Since there are so many people and teams a vocabulary that includes | 892 | Since there are so many people and teams a vocabulary that includes them |
1028 | 848 | them all is not very useful when displaying in the user interface. So | 893 | all is not very useful when displaying in the user interface. So we |
1029 | 849 | we limit the number of results. The results are ordered by | 894 | limit the number of results. The results are ordered by displayname and |
1030 | 850 | displayname and the first set of those are the ones returned | 895 | the first set of those are the ones returned |
1031 | 851 | 896 | ||
1032 | 852 | >>> login(ANONYMOUS) | 897 | >>> login(ANONYMOUS) |
1033 | 853 | >>> [person.displayname for person in vocab.search('team')] | 898 | >>> [person.displayname for person in vocab.search('team')] |
1034 | @@ -859,10 +904,12 @@ | |||
1035 | 859 | >>> login(ANONYMOUS) | 904 | >>> login(ANONYMOUS) |
1036 | 860 | >>> vocab.LIMIT | 905 | >>> vocab.LIMIT |
1037 | 861 | 100 | 906 | 100 |
1038 | 907 | |||
1039 | 862 | >>> vocab.LIMIT = 4 | 908 | >>> vocab.LIMIT = 4 |
1040 | 863 | 909 | ||
1041 | 864 | # The limit gets applied in multiple subselects, so the result | 910 | # The limit gets applied in multiple subselects, so the result |
1042 | 865 | # can actually be less than the limit. | 911 | # can actually be less than the limit. |
1043 | 912 | |||
1044 | 866 | >>> [person.displayname for person in vocab.search('team')] | 913 | >>> [person.displayname for person in vocab.search('team')] |
1045 | 867 | [u'HWDB Team', u'No Team Memberships', u'testing Spanish team'] | 914 | [u'HWDB Team', u'No Team Memberships', u'testing Spanish team'] |
1046 | 868 | 915 | ||
1047 | @@ -910,15 +957,17 @@ | |||
1048 | 910 | u'ubuntu-team', u'warty-gnome'] | 957 | u'ubuntu-team', u'warty-gnome'] |
1049 | 911 | 958 | ||
1050 | 912 | 959 | ||
1052 | 913 | === ValidTeam === | 960 | ValidTeam |
1053 | 961 | ......... | ||
1054 | 914 | 962 | ||
1055 | 915 | The valid team vocabulary is just like the ValidPersonOrTeam vocabulary, | 963 | The valid team vocabulary is just like the ValidPersonOrTeam vocabulary, |
1058 | 916 | except that its terms are limited only to teams. No non-team Persons will be | 964 | except that its terms are limited only to teams. No non-team Persons |
1059 | 917 | returned. | 965 | will be returned. |
1060 | 918 | 966 | ||
1061 | 919 | >>> vocab = get_naked_vocab(None, 'ValidTeam') | 967 | >>> vocab = get_naked_vocab(None, 'ValidTeam') |
1062 | 920 | >>> vocab.displayname | 968 | >>> vocab.displayname |
1063 | 921 | 'Select a Team' | 969 | 'Select a Team' |
1064 | 970 | |||
1065 | 922 | >>> sorted((team.displayname, team.teamowner.displayname) | 971 | >>> sorted((team.displayname, team.teamowner.displayname) |
1066 | 923 | ... for team in vocab.search(None)) | 972 | ... for team in vocab.search(None)) |
1067 | 924 | [(u'Bass Players', u'David Allouche'), | 973 | [(u'Bass Players', u'David Allouche'), |
1068 | @@ -956,8 +1005,9 @@ | |||
1069 | 956 | (u'Warty Security Team', u'Mark Shuttleworth'), | 1005 | (u'Warty Security Team', u'Mark Shuttleworth'), |
1070 | 957 | (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')] | 1006 | (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')] |
1071 | 958 | 1007 | ||
1074 | 959 | Like with ValidPersonOrTeam, you can narrow your search down by providing some | 1008 | Like with ValidPersonOrTeam, you can narrow your search down by |
1075 | 960 | text to match against the team name. Still, you only get teams back. | 1009 | providing some text to match against the team name. Still, you only get |
1076 | 1010 | teams back. | ||
1077 | 961 | 1011 | ||
1078 | 962 | >>> sorted((team.displayname, team.teamowner.displayname) | 1012 | >>> sorted((team.displayname, team.teamowner.displayname) |
1079 | 963 | ... for team in vocab.search('spanish')) | 1013 | ... for team in vocab.search('spanish')) |
1080 | @@ -987,7 +1037,8 @@ | |||
1081 | 987 | (u'Warty Security Team', u'Mark Shuttleworth'), | 1037 | (u'Warty Security Team', u'Mark Shuttleworth'), |
1082 | 988 | (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')] | 1038 | (u'testing Spanish team', u'Carlos Perell\xf3 Mar\xedn')] |
1083 | 989 | 1039 | ||
1085 | 990 | A user who is a member of a private team will see that team in his search. | 1040 | A user who is a member of a private team will see that team in his |
1086 | 1041 | search. | ||
1087 | 991 | 1042 | ||
1088 | 992 | >>> login('commercial-member@canonical.com') | 1043 | >>> login('commercial-member@canonical.com') |
1089 | 993 | >>> sorted((team.displayname, team.teamowner.displayname) | 1044 | >>> sorted((team.displayname, team.teamowner.displayname) |
1090 | @@ -1011,7 +1062,8 @@ | |||
1091 | 1011 | [(u'Ubuntu Team', u'Mark Shuttleworth')] | 1062 | [(u'Ubuntu Team', u'Mark Shuttleworth')] |
1092 | 1012 | 1063 | ||
1093 | 1013 | 1064 | ||
1095 | 1014 | === ValidPerson === | 1065 | ValidPerson |
1096 | 1066 | ........... | ||
1097 | 1015 | 1067 | ||
1098 | 1016 | All 'valid' persons who are not a team. | 1068 | All 'valid' persons who are not a team. |
1099 | 1017 | 1069 | ||
1100 | @@ -1019,19 +1071,23 @@ | |||
1101 | 1019 | >>> vocab = get_naked_vocab(None, "ValidPerson") | 1071 | >>> vocab = get_naked_vocab(None, "ValidPerson") |
1102 | 1020 | >>> vocab.displayname | 1072 | >>> vocab.displayname |
1103 | 1021 | 'Select a Person' | 1073 | 'Select a Person' |
1104 | 1074 | |||
1105 | 1022 | >>> people = vocab.search(None) | 1075 | >>> people = vocab.search(None) |
1106 | 1023 | >>> people.count() > 0 | 1076 | >>> people.count() > 0 |
1107 | 1024 | True | 1077 | True |
1108 | 1078 | |||
1109 | 1025 | >>> invalid_people = [ | 1079 | >>> invalid_people = [ |
1110 | 1026 | ... person for person in people if not person.is_valid_person] | 1080 | ... person for person in people if not person.is_valid_person] |
1111 | 1027 | >>> print len(invalid_people) | 1081 | >>> print len(invalid_people) |
1112 | 1028 | 0 | 1082 | 0 |
1113 | 1029 | 1083 | ||
1115 | 1030 | There are two 'Carlos' in the sample data but only one is a valid person. | 1084 | There are two 'Carlos' in the sample data but only one is a valid |
1116 | 1085 | person. | ||
1117 | 1031 | 1086 | ||
1118 | 1032 | >>> carlos_people = vocab.search('Carlos') | 1087 | >>> carlos_people = vocab.search('Carlos') |
1119 | 1033 | >>> print len(list(carlos_people)) | 1088 | >>> print len(list(carlos_people)) |
1120 | 1034 | 1 | 1089 | 1 |
1121 | 1090 | |||
1122 | 1035 | >>> invalid_carlos = [ | 1091 | >>> invalid_carlos = [ |
1123 | 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] |
1124 | 1037 | >>> print len(invalid_carlos) | 1093 | >>> print len(invalid_carlos) |
1125 | @@ -1039,29 +1095,27 @@ | |||
1126 | 1039 | 1095 | ||
1127 | 1040 | ValidPerson does not include teams. | 1096 | ValidPerson does not include teams. |
1128 | 1041 | 1097 | ||
1129 | 1042 | >>> # Create a new team. | ||
1130 | 1043 | >>> carlos = getUtility(IPersonSet).getByName('carlos') | 1098 | >>> carlos = getUtility(IPersonSet).getByName('carlos') |
1131 | 1044 | >>> carlos_team = factory.makeTeam( | 1099 | >>> carlos_team = factory.makeTeam( |
1132 | 1045 | ... owner=carlos, name='carlos-team') | 1100 | ... owner=carlos, name='carlos-team') |
1133 | 1046 | >>> person_or_team_vocab = get_naked_vocab(None, "ValidPersonOrTeam") | 1101 | >>> person_or_team_vocab = get_naked_vocab(None, "ValidPersonOrTeam") |
1134 | 1047 | >>> carlos_people_or_team = person_or_team_vocab.search('carlos') | 1102 | >>> carlos_people_or_team = person_or_team_vocab.search('carlos') |
1135 | 1048 | >>> # The people or team search yields our one Carlos person and | ||
1136 | 1049 | >>> # the new team. | ||
1137 | 1050 | >>> print len(list(carlos_people_or_team)) | 1103 | >>> print len(list(carlos_people_or_team)) |
1138 | 1051 | 2 | 1104 | 2 |
1139 | 1105 | |||
1140 | 1052 | >>> carlos_team in carlos_people_or_team | 1106 | >>> carlos_team in carlos_people_or_team |
1141 | 1053 | True | 1107 | True |
1142 | 1054 | 1108 | ||
1143 | 1055 | >>> # But the ValidPersonVocabulary only has the original Carlos | ||
1144 | 1056 | >>> # person, not the new team. | ||
1145 | 1057 | >>> carlos_people = vocab.search('carlos') | 1109 | >>> carlos_people = vocab.search('carlos') |
1146 | 1058 | >>> print len(list(carlos_people)) | 1110 | >>> print len(list(carlos_people)) |
1147 | 1059 | 1 | 1111 | 1 |
1148 | 1112 | |||
1149 | 1060 | >>> carlos_team in carlos_people | 1113 | >>> carlos_team in carlos_people |
1150 | 1061 | False | 1114 | False |
1151 | 1062 | 1115 | ||
1152 | 1063 | 1116 | ||
1154 | 1064 | === DistributionOrProductVocabulary === | 1117 | DistributionOrProductVocabulary |
1155 | 1118 | ............................... | ||
1156 | 1065 | 1119 | ||
1157 | 1066 | All products and distributions. Note that the value type is | 1120 | All products and distributions. Note that the value type is |
1158 | 1067 | heterogeneous. | 1121 | heterogeneous. |
1159 | @@ -1077,12 +1131,14 @@ | |||
1160 | 1077 | 1131 | ||
1161 | 1078 | >>> vocab.getTermByToken('firefox').token | 1132 | >>> vocab.getTermByToken('firefox').token |
1162 | 1079 | 'firefox' | 1133 | 'firefox' |
1163 | 1134 | |||
1164 | 1080 | >>> login('mark@example.com') | 1135 | >>> login('mark@example.com') |
1165 | 1081 | >>> product_set['firefox'].setAliases(['iceweasel']) | 1136 | >>> product_set['firefox'].setAliases(['iceweasel']) |
1166 | 1082 | >>> current_user = launchbag.user | 1137 | >>> current_user = launchbag.user |
1167 | 1083 | >>> login_person(current_user) | 1138 | >>> login_person(current_user) |
1168 | 1084 | >>> vocab.getTermByToken('iceweasel').token | 1139 | >>> vocab.getTermByToken('iceweasel').token |
1169 | 1085 | 'firefox' | 1140 | 'firefox' |
1170 | 1141 | |||
1171 | 1086 | >>> [term.token for term in vocab.searchForTerms(query='iceweasel')] | 1142 | >>> [term.token for term in vocab.searchForTerms(query='iceweasel')] |
1172 | 1087 | ['firefox'] | 1143 | ['firefox'] |
1173 | 1088 | 1144 | ||
1174 | @@ -1101,14 +1157,17 @@ | |||
1175 | 1101 | ... if 'Tomcat' in term.title: | 1157 | ... if 'Tomcat' in term.title: |
1176 | 1102 | ... print term.title, '- class', term.value.__class__.__name__ | 1158 | ... print term.title, '- class', term.value.__class__.__name__ |
1177 | 1103 | Tomcat (Product) - class Product | 1159 | Tomcat (Product) - class Product |
1178 | 1160 | |||
1179 | 1104 | >>> tomcat = product_set.getByName('tomcat') | 1161 | >>> tomcat = product_set.getByName('tomcat') |
1180 | 1105 | >>> tomcat in vocab | 1162 | >>> tomcat in vocab |
1181 | 1106 | True | 1163 | True |
1182 | 1164 | |||
1183 | 1107 | >>> tomcat.active = False | 1165 | >>> tomcat.active = False |
1184 | 1108 | >>> flush_database_updates() | 1166 | >>> flush_database_updates() |
1185 | 1109 | >>> vocab = get_naked_vocab(None, "DistributionOrProduct") | 1167 | >>> vocab = get_naked_vocab(None, "DistributionOrProduct") |
1186 | 1110 | >>> tomcat in vocab | 1168 | >>> tomcat in vocab |
1187 | 1111 | False | 1169 | False |
1188 | 1170 | |||
1189 | 1112 | >>> tomcat.active = True | 1171 | >>> tomcat.active = True |
1190 | 1113 | >>> flush_database_updates() | 1172 | >>> flush_database_updates() |
1191 | 1114 | >>> vocab = get_naked_vocab(None, "DistributionOrProduct") | 1173 | >>> vocab = get_naked_vocab(None, "DistributionOrProduct") |
1192 | @@ -1122,10 +1181,11 @@ | |||
1193 | 1122 | False | 1181 | False |
1194 | 1123 | 1182 | ||
1195 | 1124 | 1183 | ||
1197 | 1125 | === DistributionOrProductOrProjectGroupVocabulary === | 1184 | DistributionOrProductOrProjectGroupVocabulary |
1198 | 1185 | ............................................. | ||
1199 | 1126 | 1186 | ||
1202 | 1127 | All products, project groups and distributions. Note that the value type is | 1187 | All products, project groups and distributions. Note that the value type |
1203 | 1128 | heterogeneous. | 1188 | is heterogeneous. |
1204 | 1129 | 1189 | ||
1205 | 1130 | >>> vocab = get_naked_vocab(None, "DistributionOrProductOrProjectGroup") | 1190 | >>> vocab = get_naked_vocab(None, "DistributionOrProductOrProjectGroup") |
1206 | 1131 | >>> for term in vocab: | 1191 | >>> for term in vocab: |
1207 | @@ -1138,6 +1198,7 @@ | |||
1208 | 1138 | 1198 | ||
1209 | 1139 | >>> vocab.getTermByToken('ubuntu').token | 1199 | >>> vocab.getTermByToken('ubuntu').token |
1210 | 1140 | 'ubuntu' | 1200 | 'ubuntu' |
1211 | 1201 | |||
1212 | 1141 | >>> from lp.registry.interfaces.distribution import ( | 1202 | >>> from lp.registry.interfaces.distribution import ( |
1213 | 1142 | ... IDistributionSet) | 1203 | ... IDistributionSet) |
1214 | 1143 | >>> login('mark@example.com') | 1204 | >>> login('mark@example.com') |
1215 | @@ -1145,6 +1206,7 @@ | |||
1216 | 1145 | >>> login_person(current_user) | 1206 | >>> login_person(current_user) |
1217 | 1146 | >>> vocab.getTermByToken('ubantoo').token | 1207 | >>> vocab.getTermByToken('ubantoo').token |
1218 | 1147 | 'ubuntu' | 1208 | 'ubuntu' |
1219 | 1209 | |||
1220 | 1148 | >>> [term.token for term in vocab.searchForTerms(query='ubantoo')] | 1210 | >>> [term.token for term in vocab.searchForTerms(query='ubantoo')] |
1221 | 1149 | ['ubuntu'] | 1211 | ['ubuntu'] |
1222 | 1150 | 1212 | ||
1223 | @@ -1153,6 +1215,7 @@ | |||
1224 | 1153 | >>> tomcat = product_set.getByName('tomcat') | 1215 | >>> tomcat = product_set.getByName('tomcat') |
1225 | 1154 | >>> tomcat in vocab | 1216 | >>> tomcat in vocab |
1226 | 1155 | True | 1217 | True |
1227 | 1218 | |||
1228 | 1156 | >>> tomcat.active = False | 1219 | >>> tomcat.active = False |
1229 | 1157 | >>> tomcat in vocab | 1220 | >>> tomcat in vocab |
1230 | 1158 | False | 1221 | False |
1231 | @@ -1160,6 +1223,7 @@ | |||
1232 | 1160 | >>> apache = getUtility(IProjectGroupSet).getByName('apache') | 1223 | >>> apache = getUtility(IProjectGroupSet).getByName('apache') |
1233 | 1161 | >>> apache in vocab | 1224 | >>> apache in vocab |
1234 | 1162 | True | 1225 | True |
1235 | 1226 | |||
1236 | 1163 | >>> apache.active = False | 1227 | >>> apache.active = False |
1237 | 1164 | >>> apache in vocab | 1228 | >>> apache in vocab |
1238 | 1165 | False | 1229 | False |
1239 | @@ -1179,13 +1243,15 @@ | |||
1240 | 1179 | ... if 'Apache' in term.title: | 1243 | ... if 'Apache' in term.title: |
1241 | 1180 | ... print term.title, '- class', term.value.__class__.__name__ | 1244 | ... print term.title, '- class', term.value.__class__.__name__ |
1242 | 1181 | Apache (ProjectGroup) - class ProjectGroup | 1245 | Apache (ProjectGroup) - class ProjectGroup |
1243 | 1246 | |||
1244 | 1182 | >>> for term in vocab: | 1247 | >>> for term in vocab: |
1245 | 1183 | ... if 'Tomcat' in term.title: | 1248 | ... if 'Tomcat' in term.title: |
1246 | 1184 | ... print term.title, '- class', term.value.__class__.__name__ | 1249 | ... print term.title, '- class', term.value.__class__.__name__ |
1247 | 1185 | Tomcat (Product) - class Product | 1250 | Tomcat (Product) - class Product |
1248 | 1186 | 1251 | ||
1249 | 1187 | 1252 | ||
1251 | 1188 | == FeaturedProjectVocabulary == | 1253 | FeaturedProjectVocabulary |
1252 | 1254 | ------------------------- | ||
1253 | 1189 | 1255 | ||
1254 | 1190 | The featured project vocabulary contains all the projects that are | 1256 | The featured project vocabulary contains all the projects that are |
1255 | 1191 | featured on Launchpad. It is a subset of the | 1257 | featured on Launchpad. It is a subset of the |
1256 | @@ -1220,19 +1286,20 @@ | |||
1257 | 1220 | False | 1286 | False |
1258 | 1221 | 1287 | ||
1259 | 1222 | 1288 | ||
1261 | 1223 | == CommercialProjectsVocabulary == | 1289 | CommercialProjectsVocabulary |
1262 | 1290 | ---------------------------- | ||
1263 | 1224 | 1291 | ||
1264 | 1225 | The commercial projects vocabulary contains all commercial projects, | 1292 | The commercial projects vocabulary contains all commercial projects, |
1267 | 1226 | ordered by displayname. Note: a project is considered commercial if | 1293 | ordered by displayname. Note: a project is considered commercial if it |
1268 | 1227 | it has a proprietary license or no license. That's why some of these | 1294 | has a proprietary license or no license. That's why some of these |
1269 | 1228 | clearly FOSS project in our test data show up as commercial. | 1295 | clearly FOSS project in our test data show up as commercial. |
1270 | 1229 | 1296 | ||
1271 | 1230 | For a normal user (one who does not have launchpad.Commercial | 1297 | For a normal user (one who does not have launchpad.Commercial |
1274 | 1231 | permission) the owned commercial project vocabulary is a list of | 1298 | permission) the owned commercial project vocabulary is a list of project |
1275 | 1232 | project the user either owns or manages. | 1299 | the user either owns or manages. |
1276 | 1233 | 1300 | ||
1279 | 1234 | The test data has one project with a proprietary license. Let's | 1301 | The test data has one project with a proprietary license. Let's change |
1280 | 1235 | change bzr's so we will get more interesting results. | 1302 | bzr's so we will get more interesting results. |
1281 | 1236 | 1303 | ||
1282 | 1237 | >>> from lp.registry.interfaces.product import License | 1304 | >>> from lp.registry.interfaces.product import License |
1283 | 1238 | >>> bzr = product_set.getByName('bzr') | 1305 | >>> bzr = product_set.getByName('bzr') |
1284 | @@ -1284,3 +1351,5 @@ | |||
1285 | 1284 | ... print term.value.displayname | 1351 | ... print term.value.displayname |
1286 | 1285 | Bazaar | 1352 | Bazaar |
1287 | 1286 | Mega Money Maker | 1353 | Mega Money Maker |
1288 | 1354 | |||
1289 | 1355 | |||
1290 | 1287 | 1356 | ||
1291 | === modified file 'lib/lp/registry/interfaces/person.py' | |||
1292 | --- lib/lp/registry/interfaces/person.py 2011-03-01 09:41:39 +0000 | |||
1293 | +++ lib/lp/registry/interfaces/person.py 2011-03-17 20:19:01 +0000 | |||
1294 | @@ -1716,13 +1716,6 @@ | |||
1295 | 1716 | """ | 1716 | """ |
1296 | 1717 | 1717 | ||
1297 | 1718 | 1718 | ||
1298 | 1719 | class IPersonModerate(Interface): | ||
1299 | 1720 | """IPerson attributes that require launchpad.Moderate.""" | ||
1300 | 1721 | |||
1301 | 1722 | def deactivateAllMembers(comment, reviewer): | ||
1302 | 1723 | """Deactivate all the members of this team.""" | ||
1303 | 1724 | |||
1304 | 1725 | |||
1305 | 1726 | class IPersonCommAdminWriteRestricted(Interface): | 1719 | class IPersonCommAdminWriteRestricted(Interface): |
1306 | 1727 | """IPerson attributes that require launchpad.Admin permission to set.""" | 1720 | """IPerson attributes that require launchpad.Admin permission to set.""" |
1307 | 1728 | 1721 | ||
1308 | @@ -1771,7 +1764,7 @@ | |||
1309 | 1771 | 1764 | ||
1310 | 1772 | class IPerson(IPersonPublic, IPersonViewRestricted, IPersonEditRestricted, | 1765 | class IPerson(IPersonPublic, IPersonViewRestricted, IPersonEditRestricted, |
1311 | 1773 | IPersonCommAdminWriteRestricted, IPersonSpecialRestricted, | 1766 | IPersonCommAdminWriteRestricted, IPersonSpecialRestricted, |
1313 | 1774 | IPersonModerate, IHasStanding, ISetLocation, IRootContext): | 1767 | IHasStanding, ISetLocation, IRootContext): |
1314 | 1775 | """A Person.""" | 1768 | """A Person.""" |
1315 | 1776 | export_as_webservice_entry(plural_name='people') | 1769 | export_as_webservice_entry(plural_name='people') |
1316 | 1777 | 1770 | ||
1317 | 1778 | 1771 | ||
1318 | === modified file 'lib/lp/registry/interfaces/teammembership.py' | |||
1319 | --- lib/lp/registry/interfaces/teammembership.py 2011-03-01 03:52:30 +0000 | |||
1320 | +++ lib/lp/registry/interfaces/teammembership.py 2011-03-17 20:19:01 +0000 | |||
1321 | @@ -302,6 +302,16 @@ | |||
1322 | 302 | TeamMembership and I'll return None. | 302 | TeamMembership and I'll return None. |
1323 | 303 | """ | 303 | """ |
1324 | 304 | 304 | ||
1325 | 305 | def deactivateActiveMemberships(team, comment, reviewer): | ||
1326 | 306 | """Deactivate all team members in ACTIVE_STATES. | ||
1327 | 307 | |||
1328 | 308 | This is a convenience method used before teams are deleted. | ||
1329 | 309 | |||
1330 | 310 | :param team: The team to deactivate. | ||
1331 | 311 | :param comment: An explanation for the deactivation. | ||
1332 | 312 | :param reviewer: The user doing the deactivation. | ||
1333 | 313 | """ | ||
1334 | 314 | |||
1335 | 305 | 315 | ||
1336 | 306 | class ITeamParticipation(Interface): | 316 | class ITeamParticipation(Interface): |
1337 | 307 | """A TeamParticipation. | 317 | """A TeamParticipation. |
1338 | 308 | 318 | ||
1339 | === modified file 'lib/lp/registry/model/person.py' | |||
1340 | --- lib/lp/registry/model/person.py 2011-03-09 18:18:02 +0000 | |||
1341 | +++ lib/lp/registry/model/person.py 2011-03-17 20:19:01 +0000 | |||
1342 | @@ -246,10 +246,7 @@ | |||
1343 | 246 | SSHKeyCompromisedError, | 246 | SSHKeyCompromisedError, |
1344 | 247 | SSHKeyType, | 247 | SSHKeyType, |
1345 | 248 | ) | 248 | ) |
1350 | 249 | from lp.registry.interfaces.teammembership import ( | 249 | from lp.registry.interfaces.teammembership import TeamMembershipStatus |
1347 | 250 | ACTIVE_STATES, | ||
1348 | 251 | TeamMembershipStatus, | ||
1349 | 252 | ) | ||
1351 | 253 | from lp.registry.interfaces.wikiname import ( | 250 | from lp.registry.interfaces.wikiname import ( |
1352 | 254 | IWikiName, | 251 | IWikiName, |
1353 | 255 | IWikiNameSet, | 252 | IWikiNameSet, |
1354 | @@ -1547,89 +1544,6 @@ | |||
1355 | 1547 | tm.dateexpires += timedelta(days=team.defaultrenewalperiod) | 1544 | tm.dateexpires += timedelta(days=team.defaultrenewalperiod) |
1356 | 1548 | tm.sendSelfRenewalNotification() | 1545 | tm.sendSelfRenewalNotification() |
1357 | 1549 | 1546 | ||
1358 | 1550 | def deactivateAllMembers(self, comment, reviewer): | ||
1359 | 1551 | """Deactivate all members of this team. | ||
1360 | 1552 | |||
1361 | 1553 | This method circuments the TeamMembership.setStatus() method | ||
1362 | 1554 | to improve performance; therefore, it does not send out any | ||
1363 | 1555 | status change noticiations to the team admins. | ||
1364 | 1556 | |||
1365 | 1557 | :param comment: Explanation of the change. | ||
1366 | 1558 | :param reviewer: Person who made the change. | ||
1367 | 1559 | """ | ||
1368 | 1560 | assert self.is_team, "This method is only available for teams." | ||
1369 | 1561 | now = datetime.now(pytz.timezone('UTC')) | ||
1370 | 1562 | store = Store.of(self) | ||
1371 | 1563 | cur = cursor() | ||
1372 | 1564 | |||
1373 | 1565 | # Deactivate the approved/admin team members. | ||
1374 | 1566 | # XXX: EdwinGrubbs 2009-07-08 bug=397072 | ||
1375 | 1567 | # There are problems using storm to write an update | ||
1376 | 1568 | # statement using DBITems in the comparison. | ||
1377 | 1569 | cur.execute(""" | ||
1378 | 1570 | UPDATE TeamMembership | ||
1379 | 1571 | SET status=%(status)s, | ||
1380 | 1572 | last_changed_by=%(last_changed_by)s, | ||
1381 | 1573 | last_change_comment=%(comment)s, | ||
1382 | 1574 | date_last_changed=%(date_last_changed)s | ||
1383 | 1575 | WHERE | ||
1384 | 1576 | TeamMembership.team = %(team)s | ||
1385 | 1577 | AND TeamMembership.status IN %(original_statuses)s | ||
1386 | 1578 | """, | ||
1387 | 1579 | dict( | ||
1388 | 1580 | status=TeamMembershipStatus.DEACTIVATED, | ||
1389 | 1581 | last_changed_by=reviewer.id, | ||
1390 | 1582 | comment=comment, | ||
1391 | 1583 | date_last_changed=now, | ||
1392 | 1584 | team=self.id, | ||
1393 | 1585 | original_statuses=( | ||
1394 | 1586 | TeamMembershipStatus.ADMIN.value, | ||
1395 | 1587 | TeamMembershipStatus.APPROVED.value))) | ||
1396 | 1588 | |||
1397 | 1589 | # Since we've updated the database behind Storm's back, | ||
1398 | 1590 | # flush its caches. | ||
1399 | 1591 | store.invalidate() | ||
1400 | 1592 | |||
1401 | 1593 | # Remove all indirect TeamParticipation entries resulting from this | ||
1402 | 1594 | # team. If this were just a select, it would be a complicated but | ||
1403 | 1595 | # feasible set of joins. Since it's a delete, we have to use | ||
1404 | 1596 | # some sub selects. | ||
1405 | 1597 | cur.execute(''' | ||
1406 | 1598 | DELETE FROM TeamParticipation | ||
1407 | 1599 | WHERE | ||
1408 | 1600 | -- The person needs to be a member of the team in question | ||
1409 | 1601 | person IN | ||
1410 | 1602 | (SELECT person from TeamParticipation WHERE | ||
1411 | 1603 | team = %(team)s) AND | ||
1412 | 1604 | |||
1413 | 1605 | -- The teams being deleted should be teams that this team | ||
1414 | 1606 | -- is a member of. | ||
1415 | 1607 | team IN | ||
1416 | 1608 | (SELECT team from TeamMembership WHERE | ||
1417 | 1609 | person = %(team)s) AND | ||
1418 | 1610 | |||
1419 | 1611 | -- The person needs to not have direct membership in the | ||
1420 | 1612 | -- team. | ||
1421 | 1613 | NOT EXISTS | ||
1422 | 1614 | (SELECT tm1.person from TeamMembership tm1 | ||
1423 | 1615 | WHERE | ||
1424 | 1616 | tm1.person = TeamParticipation.person and | ||
1425 | 1617 | tm1.team = TeamParticipation.team and | ||
1426 | 1618 | tm1.status IN %(active_states)s); | ||
1427 | 1619 | ''', dict(team=self.id, active_states=ACTIVE_STATES)) | ||
1428 | 1620 | |||
1429 | 1621 | # Since we've updated the database behind Storm's back yet again, | ||
1430 | 1622 | # we need to flush its caches, again. | ||
1431 | 1623 | store.invalidate() | ||
1432 | 1624 | |||
1433 | 1625 | # Remove all members from the TeamParticipation table | ||
1434 | 1626 | # except for the team, itself. | ||
1435 | 1627 | participants = store.find( | ||
1436 | 1628 | TeamParticipation, | ||
1437 | 1629 | TeamParticipation.teamID == self.id, | ||
1438 | 1630 | TeamParticipation.personID != self.id) | ||
1439 | 1631 | participants.remove() | ||
1440 | 1632 | |||
1441 | 1633 | def setMembershipData(self, person, status, reviewer, expires=None, | 1547 | def setMembershipData(self, person, status, reviewer, expires=None, |
1442 | 1634 | comment=None): | 1548 | comment=None): |
1443 | 1635 | """See `IPerson`.""" | 1549 | """See `IPerson`.""" |
1444 | 1636 | 1550 | ||
1445 | === modified file 'lib/lp/registry/model/teammembership.py' | |||
1446 | --- lib/lp/registry/model/teammembership.py 2011-03-09 18:18:02 +0000 | |||
1447 | +++ lib/lp/registry/model/teammembership.py 2011-03-17 20:19:01 +0000 | |||
1448 | @@ -30,6 +30,7 @@ | |||
1449 | 30 | from canonical.database.datetimecol import UtcDateTimeCol | 30 | from canonical.database.datetimecol import UtcDateTimeCol |
1450 | 31 | from canonical.database.enumcol import EnumCol | 31 | from canonical.database.enumcol import EnumCol |
1451 | 32 | from canonical.database.sqlbase import ( | 32 | from canonical.database.sqlbase import ( |
1452 | 33 | cursor, | ||
1453 | 33 | flush_database_updates, | 34 | flush_database_updates, |
1454 | 34 | SQLBase, | 35 | SQLBase, |
1455 | 35 | sqlvalues, | 36 | sqlvalues, |
1456 | @@ -493,6 +494,33 @@ | |||
1457 | 493 | TeamMembershipRenewalPolicy.AUTOMATIC) | 494 | TeamMembershipRenewalPolicy.AUTOMATIC) |
1458 | 494 | return IStore(TeamMembership).find(TeamMembership, *conditions) | 495 | return IStore(TeamMembership).find(TeamMembership, *conditions) |
1459 | 495 | 496 | ||
1460 | 497 | def deactivateActiveMemberships(self, team, comment, reviewer): | ||
1461 | 498 | """See `ITeamMembershipSet`.""" | ||
1462 | 499 | now = datetime.now(pytz.timezone('UTC')) | ||
1463 | 500 | store = Store.of(team) | ||
1464 | 501 | cur = cursor() | ||
1465 | 502 | all_members = list(team.activemembers) | ||
1466 | 503 | cur.execute(""" | ||
1467 | 504 | UPDATE TeamMembership | ||
1468 | 505 | SET status=%(status)s, | ||
1469 | 506 | last_changed_by=%(last_changed_by)s, | ||
1470 | 507 | last_change_comment=%(comment)s, | ||
1471 | 508 | date_last_changed=%(date_last_changed)s | ||
1472 | 509 | WHERE | ||
1473 | 510 | TeamMembership.team = %(team)s | ||
1474 | 511 | AND TeamMembership.status IN %(original_statuses)s | ||
1475 | 512 | """, | ||
1476 | 513 | dict( | ||
1477 | 514 | status=TeamMembershipStatus.DEACTIVATED, | ||
1478 | 515 | last_changed_by=reviewer.id, | ||
1479 | 516 | comment=comment, | ||
1480 | 517 | date_last_changed=now, | ||
1481 | 518 | team=team.id, | ||
1482 | 519 | original_statuses=ACTIVE_STATES)) | ||
1483 | 520 | for member in all_members: | ||
1484 | 521 | # store.invalidate() is called for each iteration. | ||
1485 | 522 | _cleanTeamParticipation(member, team) | ||
1486 | 523 | |||
1487 | 496 | 524 | ||
1488 | 497 | class TeamParticipation(SQLBase): | 525 | class TeamParticipation(SQLBase): |
1489 | 498 | implements(ITeamParticipation) | 526 | implements(ITeamParticipation) |
1490 | 499 | 527 | ||
1491 | === modified file 'lib/lp/registry/tests/test_person.py' | |||
1492 | --- lib/lp/registry/tests/test_person.py 2011-03-08 22:31:58 +0000 | |||
1493 | +++ lib/lp/registry/tests/test_person.py 2011-03-17 20:19:01 +0000 | |||
1494 | @@ -55,6 +55,7 @@ | |||
1495 | 55 | PersonVisibility, | 55 | PersonVisibility, |
1496 | 56 | ) | 56 | ) |
1497 | 57 | from lp.registry.interfaces.product import IProductSet | 57 | from lp.registry.interfaces.product import IProductSet |
1498 | 58 | from lp.registry.interfaces.teammembership import ITeamMembershipSet | ||
1499 | 58 | from lp.registry.model.karma import ( | 59 | from lp.registry.model.karma import ( |
1500 | 59 | KarmaCategory, | 60 | KarmaCategory, |
1501 | 60 | KarmaTotalCache, | 61 | KarmaTotalCache, |
1502 | @@ -684,8 +685,9 @@ | |||
1503 | 684 | self.assertEqual(oldest_date, person.datecreated) | 685 | self.assertEqual(oldest_date, person.datecreated) |
1504 | 685 | 686 | ||
1505 | 686 | def _doMerge(self, test_team, target_team): | 687 | def _doMerge(self, test_team, target_team): |
1508 | 687 | test_team.deactivateAllMembers( | 688 | membershipset = getUtility(ITeamMembershipSet) |
1509 | 688 | comment='', | 689 | membershipset.deactivateActiveMemberships( |
1510 | 690 | test_team, comment='', | ||
1511 | 689 | reviewer=test_team.teamowner) | 691 | reviewer=test_team.teamowner) |
1512 | 690 | self.person_set.merge(test_team, target_team) | 692 | self.person_set.merge(test_team, target_team) |
1513 | 691 | 693 | ||
1514 | 692 | 694 | ||
1515 | === modified file 'lib/lp/registry/tests/test_team.py' | |||
1516 | --- lib/lp/registry/tests/test_team.py 2011-02-01 19:16:43 +0000 | |||
1517 | +++ lib/lp/registry/tests/test_team.py 2011-03-17 20:19:01 +0000 | |||
1518 | @@ -425,68 +425,3 @@ | |||
1519 | 425 | members = list(team.approvedmembers) | 425 | members = list(team.approvedmembers) |
1520 | 426 | self.assertEqual(1, len(members)) | 426 | self.assertEqual(1, len(members)) |
1521 | 427 | self.assertEqual(user, members[0]) | 427 | self.assertEqual(user, members[0]) |
1522 | 428 | |||
1523 | 429 | |||
1524 | 430 | class TestMembershipManagement(TestCaseWithFactory): | ||
1525 | 431 | |||
1526 | 432 | layer = DatabaseFunctionalLayer | ||
1527 | 433 | |||
1528 | 434 | def test_deactivateAllMembers_cleans_up_teamparticipation_deactivated( | ||
1529 | 435 | self): | ||
1530 | 436 | superteam = self.factory.makeTeam(name='super') | ||
1531 | 437 | targetteam = self.factory.makeTeam(name='target') | ||
1532 | 438 | login_celebrity('admin') | ||
1533 | 439 | targetteam.join(superteam, targetteam.teamowner) | ||
1534 | 440 | |||
1535 | 441 | # Now we create a deactivated link for the target team's teamowner. | ||
1536 | 442 | targetteam.teamowner.join(superteam, targetteam.teamowner) | ||
1537 | 443 | targetteam.teamowner.leave(superteam) | ||
1538 | 444 | |||
1539 | 445 | self.assertEqual( | ||
1540 | 446 | sorted([superteam, targetteam]), | ||
1541 | 447 | sorted([team for team in | ||
1542 | 448 | targetteam.teamowner.teams_participated_in])) | ||
1543 | 449 | targetteam.deactivateAllMembers( | ||
1544 | 450 | comment='test', | ||
1545 | 451 | reviewer=targetteam.teamowner) | ||
1546 | 452 | self.assertEqual( | ||
1547 | 453 | [], | ||
1548 | 454 | sorted([team for team in | ||
1549 | 455 | targetteam.teamowner.teams_participated_in])) | ||
1550 | 456 | |||
1551 | 457 | def test_deactivateAllMembers_cleans_up_teamparticipation_teamowner(self): | ||
1552 | 458 | superteam = self.factory.makeTeam(name='super') | ||
1553 | 459 | targetteam = self.factory.makeTeam(name='target') | ||
1554 | 460 | login_celebrity('admin') | ||
1555 | 461 | targetteam.join(superteam, targetteam.teamowner) | ||
1556 | 462 | self.assertEqual( | ||
1557 | 463 | sorted([superteam, targetteam]), | ||
1558 | 464 | sorted([team for team | ||
1559 | 465 | in targetteam.teamowner.teams_participated_in])) | ||
1560 | 466 | targetteam.deactivateAllMembers( | ||
1561 | 467 | comment='test', | ||
1562 | 468 | reviewer=targetteam.teamowner) | ||
1563 | 469 | self.assertEqual( | ||
1564 | 470 | [], | ||
1565 | 471 | sorted([team for team | ||
1566 | 472 | in targetteam.teamowner.teams_participated_in])) | ||
1567 | 473 | |||
1568 | 474 | def test_deactivateAllMembers_cleans_up_team_participation(self): | ||
1569 | 475 | superteam = self.factory.makeTeam(name='super') | ||
1570 | 476 | sharedteam = self.factory.makeTeam(name='shared') | ||
1571 | 477 | anotherteam = self.factory.makeTeam(name='another') | ||
1572 | 478 | targetteam = self.factory.makeTeam(name='target') | ||
1573 | 479 | person = self.factory.makePerson() | ||
1574 | 480 | login_celebrity('admin') | ||
1575 | 481 | person.join(targetteam) | ||
1576 | 482 | person.join(sharedteam) | ||
1577 | 483 | person.join(anotherteam) | ||
1578 | 484 | targetteam.join(superteam, targetteam.teamowner) | ||
1579 | 485 | targetteam.join(sharedteam, targetteam.teamowner) | ||
1580 | 486 | self.assertTrue(superteam in person.teams_participated_in) | ||
1581 | 487 | targetteam.deactivateAllMembers( | ||
1582 | 488 | comment='test', | ||
1583 | 489 | reviewer=targetteam.teamowner) | ||
1584 | 490 | self.assertEqual( | ||
1585 | 491 | sorted([sharedteam, anotherteam]), | ||
1586 | 492 | sorted([team for team in person.teams_participated_in])) | ||
1587 | 493 | 428 | ||
1588 | === modified file 'lib/lp/registry/tests/test_teammembership.py' | |||
1589 | --- lib/lp/registry/tests/test_teammembership.py 2011-03-01 03:52:30 +0000 | |||
1590 | +++ lib/lp/registry/tests/test_teammembership.py 2011-03-17 20:19:01 +0000 | |||
1591 | @@ -51,16 +51,18 @@ | |||
1592 | 51 | TeamParticipation, | 51 | TeamParticipation, |
1593 | 52 | ) | 52 | ) |
1594 | 53 | from lp.testing import ( | 53 | from lp.testing import ( |
1595 | 54 | login_celebrity, | ||
1596 | 54 | person_logged_in, | 55 | person_logged_in, |
1597 | 55 | TestCaseWithFactory, | 56 | TestCaseWithFactory, |
1598 | 56 | ) | 57 | ) |
1599 | 57 | from lp.testing.mail_helpers import pop_notifications | 58 | from lp.testing.mail_helpers import pop_notifications |
1600 | 58 | 59 | ||
1601 | 59 | 60 | ||
1603 | 60 | class TestTeamMembershipSet(TestCase): | 61 | class TestTeamMembershipSet(TestCaseWithFactory): |
1604 | 61 | layer = DatabaseFunctionalLayer | 62 | layer = DatabaseFunctionalLayer |
1605 | 62 | 63 | ||
1606 | 63 | def setUp(self): | 64 | def setUp(self): |
1607 | 65 | super(TestTeamMembershipSet, self).setUp() | ||
1608 | 64 | login('test@canonical.com') | 66 | login('test@canonical.com') |
1609 | 65 | self.membershipset = getUtility(ITeamMembershipSet) | 67 | self.membershipset = getUtility(ITeamMembershipSet) |
1610 | 66 | self.personset = getUtility(IPersonSet) | 68 | self.personset = getUtility(IPersonSet) |
1611 | @@ -166,6 +168,24 @@ | |||
1612 | 166 | sample_person_on_motu.status, TeamMembershipStatus.EXPIRED) | 168 | sample_person_on_motu.status, TeamMembershipStatus.EXPIRED) |
1613 | 167 | self.failIf(sample_person.inTeam(motu)) | 169 | self.failIf(sample_person.inTeam(motu)) |
1614 | 168 | 170 | ||
1615 | 171 | def test_deactivateActiveMemberships(self): | ||
1616 | 172 | superteam = self.factory.makeTeam(name='super') | ||
1617 | 173 | targetteam = self.factory.makeTeam(name='target') | ||
1618 | 174 | member = self.factory.makePerson() | ||
1619 | 175 | login_celebrity('admin') | ||
1620 | 176 | targetteam.join(superteam, targetteam.teamowner) | ||
1621 | 177 | targetteam.addMember(member, targetteam.teamowner) | ||
1622 | 178 | targetteam.teamowner.join(superteam, targetteam.teamowner) | ||
1623 | 179 | self.membershipset.deactivateActiveMemberships( | ||
1624 | 180 | targetteam, comment='test', reviewer=targetteam.teamowner) | ||
1625 | 181 | membership = self.membershipset.getByPersonAndTeam(member, targetteam) | ||
1626 | 182 | self.assertEqual('test', membership.last_change_comment) | ||
1627 | 183 | self.assertEqual(targetteam.teamowner, membership.last_changed_by) | ||
1628 | 184 | self.assertEqual([], list(targetteam.allmembers)) | ||
1629 | 185 | self.assertEqual( | ||
1630 | 186 | [superteam], list(targetteam.teamowner.teams_participated_in)) | ||
1631 | 187 | self.assertEqual([], list(member.teams_participated_in)) | ||
1632 | 188 | |||
1633 | 169 | 189 | ||
1634 | 170 | class TeamParticipationTestCase(TestCaseWithFactory): | 190 | class TeamParticipationTestCase(TestCaseWithFactory): |
1635 | 171 | """Tests for team participation using 5 teams.""" | 191 | """Tests for team participation using 5 teams.""" |
1636 | @@ -465,6 +485,31 @@ | |||
1637 | 465 | previous_count-10, | 485 | previous_count-10, |
1638 | 466 | self.getTeamParticipationCount()) | 486 | self.getTeamParticipationCount()) |
1639 | 467 | 487 | ||
1640 | 488 | def testTeam3_deactivateActiveMemberships(self): | ||
1641 | 489 | # Removing all the members of team2 will not remove memberships | ||
1642 | 490 | # to super teams from other paths. | ||
1643 | 491 | non_member = self.factory.makePerson() | ||
1644 | 492 | self.team3.addMember(non_member, self.foo_bar, force_team_add=True) | ||
1645 | 493 | previous_count = self.getTeamParticipationCount() | ||
1646 | 494 | membershipset = getUtility(ITeamMembershipSet) | ||
1647 | 495 | membershipset.deactivateActiveMemberships( | ||
1648 | 496 | self.team3, 'gone', self.foo_bar) | ||
1649 | 497 | self.assertEqual([], list(self.team3.allmembers)) | ||
1650 | 498 | self.assertParticipantsEquals( | ||
1651 | 499 | ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'], | ||
1652 | 500 | self.team1) | ||
1653 | 501 | self.assertParticipantsEquals( | ||
1654 | 502 | ['name16', 'no-priv', 'team3', 'team4', 'team5'], self.team2) | ||
1655 | 503 | self.assertParticipantsEquals( | ||
1656 | 504 | [], self.team3) | ||
1657 | 505 | self.assertParticipantsEquals( | ||
1658 | 506 | ['name16', 'no-priv', 'team5'], self.team4) | ||
1659 | 507 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | ||
1660 | 508 | self.assertParticipantsEquals( | ||
1661 | 509 | ['name16', 'no-priv', 'team2', 'team3', 'team4', 'team5'], | ||
1662 | 510 | self.team6) | ||
1663 | 511 | self.assertEqual(previous_count - 8, self.getTeamParticipationCount()) | ||
1664 | 512 | |||
1665 | 468 | 513 | ||
1666 | 469 | class TestTeamMembership(TestCaseWithFactory): | 514 | class TestTeamMembership(TestCaseWithFactory): |
1667 | 470 | layer = DatabaseFunctionalLayer | 515 | layer = DatabaseFunctionalLayer |
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.