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