~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
==================
Team Mailing Lists
==================

Teams may have at most one team mailing list.  Creating a team mailing list
requires several steps, starting with registration of the list by the owner of
an existing team.  This is done through an IMailingListSet utility.

    >>> from lp.services.webapp.testing import verifyObject
    >>> from lp.registry.interfaces.mailinglist import (
    ...     IMailingList,
    ...     IMailingListSet,
    ...     )
    >>> list_set = getUtility(IMailingListSet)
    >>> verifyObject(IMailingListSet, list_set)
    True

In the following description of how to use team mailing lists, we will need
several some teams.

    >>> from lp.registry.tests.mailinglists_helper import new_team
    >>> team_one = new_team('team-one')
    >>> team_two = new_team('team-two')

    # Define a helper function that sorts mailing lists alphabetically based
    # on their team's name.  We can't use operator.attrgetter() because until
    # Python 2.6, that traverses only one level of attribute.
    >>> def sorted_lists(lists):
    ...     return sorted(lists, key=lambda L: L.team.name)

None of these teams have mailing lists yet.

    >>> sorted_lists(list_set.approved_lists)
    []
    >>> sorted_lists(list_set.active_lists)
    []
    >>> print list_set.get(team_one.name)
    None
    >>> print list_set.get(team_two.name)
    None


Creating a team mailing list
============================

When a mailing list is created, it is automatically set to the APPROVED state,
meaning that list requests are automatically approved.  This doesn't actually
create the list until Mailman acts on the mailing list creation request.

    >>> list_one = list_set.new(team_one)
    >>> verifyObject(IMailingList, list_one)
    True
    >>> list_one
    <MailingList for team "team-one"; status=APPROVED;
     address=team-one@lists.launchpad.dev at 0x...>

You can always access the mailing list through its team, if the team has a
mailing list.

    >>> team_one.mailing_list
    <MailingList for team "team-one"; status=APPROVED; ...>
    >>> print team_two.mailing_list
    None

You may not register a mailing list for a person.

    >>> login('foo.bar@canonical.com')
    >>> anne = factory.makePersonByName('Anne')
    >>> login(ANONYMOUS)
    >>> list_set.new(anne, anne)
    Traceback (most recent call last):
    ...
    AssertionError: Cannot register a list for a person who is not a team

The mailing list registrant must be a team owner or administrator.  Anne is
neither and thus may not create a list for team_two.

    >>> list_set.new(team_two, anne)
    Traceback (most recent call last):
    ...
    AssertionError: registrant is not a team owner or administrator

However, if we make Anne a team owner, she can create the mailing list.

    >>> from lp.registry.interfaces.teammembership import TeamMembershipStatus
    >>> login('foo.bar@canonical.com')
    >>> bart = factory.makePersonByName('Bart')
    >>> ignored = team_two.addMember(
    ...     anne, bart, status=TeamMembershipStatus.ADMIN)
    >>> login(ANONYMOUS)
    >>> list_two = list_set.new(team_two, anne)
    >>> list_two
    <MailingList for team "team-two"; status=APPROVED; ...>
    >>> print list_two.address
    team-two@lists.launchpad.dev

The newly registered mailing list is linked to its team, and the list's
registrant is the team owner at the time the list was registered.  The list's
status is set to APPROVED.

    >>> print list_one.team.displayname
    Team One
    >>> print list_one.registrant.name
    no-priv
    >>> print list_one.status.name
    APPROVED

The mailing list has no activation date or welcome message text yet.

    >>> print list_one.date_activated
    None
    >>> print list_one.welcome_message
    None

A mailing list cannot be registered more than once.

    >>> list_set.new(team_one)
    Traceback (most recent call last):
    ...
    AssertionError: Mailing list for team "team-one" already exists


Constructing mailing lists
==========================

Once a team mailing list has been approved, it can be constructed by Mailman.
This happens by returning the set of approved mailing lists through the XMLRPC
interface used by Mailman (not shown here).  When Mailman retrieves the set of
mailing lists to construct, the list's statuses are set to the CONSTRUCTING
state.

    >>> sorted_lists(list_set.approved_lists)
    [<MailingList for team "team-one"; status=APPROVED; ...>,
     <MailingList for team "team-two"; status=APPROVED; ...>]

    >>> list_one.startConstructing()
    >>> print list_one.status.name
    CONSTRUCTING

Once in the construction phase, a list is no longer in the approval state.

    >>> sorted_lists(list_set.approved_lists)
    [<MailingList for team "team-two"; status=APPROVED; ...>]

Lists should never be constructed more than once.

    >>> list_one.startConstructing()
    Traceback (most recent call last):
    ...
    AssertionError: Only approved mailing lists may be constructed

Construct another list for later.

    >>> list_two.startConstructing()


Reporting the results of construction
=====================================

After Mailman has worked at constructing lists for a while, it reports (again
through XMLRPC not shown here) on the status of each list construction.  Most,
if not all will succeed, thus activating the team's mailing list. Also, once
a mailing list is made active, its email address is registered in Launchpad
and associated with the team's mailing list, so that it can be used as the
team's contact address.

    >>> from lp.services.identity.interfaces.emailaddress import (
    ...     IEmailAddressSet)
    >>> email_set = getUtility(IEmailAddressSet)
    >>> print email_set.getByEmail(list_one.address)
    None
    >>> print list_one.date_activated
    None

    >>> from lp.registry.interfaces.mailinglist import MailingListStatus
    >>> list_one.transitionToStatus(MailingListStatus.ACTIVE)
    >>> print list_one.status.name
    ACTIVE
    >>> print email_set.getByEmail(list_one.address).status.name
    VALIDATED
    >>> from lp.services.database.sqlbase import get_transaction_timestamp
    >>> transaction_timestamp = get_transaction_timestamp()
    >>> list_one.date_activated == transaction_timestamp
    True

Some list constructions may fail.

    >>> list_two.transitionToStatus(MailingListStatus.FAILED)
    >>> print list_two.status.name
    FAILED

You can then get the mailing list for a team, given the team name.

    >>> list_set.get(team_one.name)
    <MailingList for team "team-one"; status=ACTIVE; ...>
    >>> list_set.get(team_two.name)
    <MailingList for team "team-two"; status=FAILED; ...>

This method will return None for missing teams or non-team people.

    >>> print list_set.get('not an existing team')
    None
    >>> print list_set.get('salgado')
    None


Deactivating lists
==================

A list which is active may be deactivated.

    >>> team_three = new_team('team-three')
    >>> list_three = list_set.new(team_three)
    >>> list_three.startConstructing()
    >>> list_three.transitionToStatus(MailingListStatus.ACTIVE)
    >>> transaction.commit()

    >>> login_person(team_three.teamowner)
    >>> list_three.deactivate()
    >>> print list_three.status.name
    DEACTIVATING

This doesn't immediately deactivate the mailing list though.  Mailman still
needs to query for the requested deactivations, take the necessary actions,
and report the deactivation results.

    >>> sorted_lists(list_set.deactivated_lists)
    [<MailingList for team "team-three"; status=DEACTIVATING; ...>]
    >>> list_three.transitionToStatus(MailingListStatus.INACTIVE)
    >>> print list_three.status.name
    INACTIVE

Once a list's deactivation is complete, the status of its email address is set
to NEW.

    >>> print email_set.getByEmail(list_three.address).status.name
    NEW

But lists which are not active may not be deactivated.

    >>> list_three.deactivate()
    Traceback (most recent call last):
    ...
    AssertionError: Only active mailing lists may be deactivated
    >>> list_two.deactivate()
    Traceback (most recent call last):
    ...
    AssertionError: Only active mailing lists may be deactivated


Reactivating lists
==================

A list which is inactive may be reactivated.

    >>> print list_three.status.name
    INACTIVE
    >>> list_three.reactivate()

This doesn't immediately reactivate the mailing list though.  Mailman still
needs to query for the requested reactivations, take the necessary actions,
and report the reactivation results.

    >>> print list_three.status.name
    APPROVED

But lists which are not inactive may not be reactivated.

    >>> list_three.reactivate()
    Traceback (most recent call last):
    ...
    AssertionError: Only inactive mailing lists may be reactivated


Mailing list permissions
========================

Permissions on a team's mailing list are not tracked separately from
permissions on the team.

You cannot turn a team with a mailing list into a private team.

    >>> from zope.component import queryAdapter
    >>> from lp.services.privacy.interfaces import IObjectPrivacy
    >>> from lp.registry.interfaces.person import PersonVisibility
    >>> queryAdapter(team_one, IObjectPrivacy).is_private
    False
    >>> login('foo.bar@canonical.com')
    >>> team_one.visibility = PersonVisibility.PRIVATE
    Traceback (most recent call last):
    ...
    ImmutableVisibilityError: This team cannot be converted to
    Private since it is referenced by a mailing list.

However, you can give a private team a mailing list.

    >>> thereminists = new_team('thereminists')
    >>> queryAdapter(thereminists, IObjectPrivacy).is_private
    False
    >>> thereminists.visibility = PersonVisibility.PRIVATE

    >>> queryAdapter(thereminists, IObjectPrivacy).is_private
    True
    >>> from lp.registry.tests.mailinglists_helper import (
    ...     new_list_for_team)
    >>> thereminists_list = new_list_for_team(thereminists)
    >>> thereminists_list
    <MailingList for team "thereminists"; status=ACTIVE; ...>

Welcome messages
================

Mailing lists have a welcome message text which is sent to new members when
they subscribe to a list.  The welcome message can contain any text.

    >>> print list_one.welcome_message
    None
    >>> list_one.welcome_message = """\
    ... Welcome to the Team One mailing list."""
    >>> login(ANONYMOUS)
    >>> print list_one.welcome_message
    Welcome to the Team One mailing list.

After changing the welcome message, the list's status should be MODIFIED.

    >>> print list_one.status.name
    MODIFIED
    >>> sorted_lists(list_set.modified_lists)
    [<MailingList for team "team-one"; status=MODIFIED; ...>]

Eventually, Mailman will get around to acting on this modification.  When it
does so, the list's state transitions first to UPDATING so as to avoid
multiple modifications.  Transitioning to the ACTIVE state while still
MODIFIED is not allowed.

    >>> list_one.transitionToStatus(MailingListStatus.ACTIVE)
    Traceback (most recent call last):
    ...
    AssertionError: Not a valid state transition: Modified -> Active

What really happens is that the list's state is first transitioned to
UPDATING, and then to ACTIVE or FAILED.

    >>> list_one.startUpdating()
    >>> print list_one.status.name
    UPDATING
    >>> list_one.transitionToStatus(MailingListStatus.ACTIVE)
    >>> print list_one.status.name
    ACTIVE

You cannot change the welcome message text for a mailing list in anything but
the ACTIVE status.

    >>> login('foo.bar@canonical.com')
    >>> list_two.welcome_message = """\
    ... This list has been declined."""
    Traceback (most recent call last):
    ...
    AssertionError: Only usable mailing lists may be modified

    >>> list_three.welcome_message = """\
    ... This list has been deactivated."""
    Traceback (most recent call last):
    ...
    AssertionError: Only usable mailing lists may be modified


Renaming teams with mailing lists
=================================

A team that has a mailing list may not be renamed.

    >>> login('no-priv@canonical.com')
    >>> team_one.name = 'team-canonical'
    Traceback (most recent call last):
    ...
    AssertionError: Cannot rename teams with mailing lists

But a team with no mailing list (yet) can still be renamed.

    >>> team_six = new_team('team-six')
    >>> team_six.name = 'team-canonical'
    >>> print team_six.name
    team-canonical


Team archive links
==================

Mailing lists have archives, accessible through a list-specific url.  However,
if a mailing list has never be activated, it won't have an archive url.

    >>> print list_two.archive_url
    None

An active mailing list has an archive url.

    >>> print list_one.status.name
    ACTIVE
    >>> print list_one.archive_url
    http://lists.launchpad.dev/team-one

Inactive mailing lists also have an archive url, because once activated, a
mailing list could have an archive and archives are never deleted.

    >>> list_one.deactivate()
    >>> list_one.transitionToStatus(MailingListStatus.INACTIVE)
    >>> print list_one.status.name
    INACTIVE
    >>> print list_one.archive_url
    http://lists.launchpad.dev/team-one


Events
======

Activating the mailing list (changing it's status to 'available for
subscription') will fire an instance of the ObjectModifiedEvent.

    # Register an event listener that will print event it receives.
    >>> from lp.testing.event import TestEventListener
    >>> from lazr.lifecycle.interfaces import IObjectModifiedEvent
    >>> from lp.registry.interfaces.mailinglist import IMailingList
    >>> def print_event(object, event):
    ...     print "Received %s on %s" % (
    ...         event.__class__.__name__.split('.')[-1],
    ...         object.__class__.__name__.split('.')[-1])
    >>> mailinglist_event_listener = TestEventListener(
    ...     IMailingList, IObjectModifiedEvent, print_event)


    # We need to build a new mailing list to use in our tests
    >>> list_six = list_set.new(team_six)
    >>> list_six.startConstructing()
    >>> print list_six.status.name
    CONSTRUCTING

    >>> list_six.transitionToStatus(MailingListStatus.ACTIVE)
    Received ObjectModifiedEvent on MailingList
    >>> print list_six.status.name
    ACTIVE

    # Cleanup
    >>> mailinglist_event_listener.unregister()


Purging
=======

There are times when we want to perform certain actions that normally are
unsafe to do when a team has a mailing list.  For example, we might want to
merge a team with a mailing list into another team, or we might want to allow
a team owner to re-request a mailing list that was incorrectly declined.

In order to support this, mailing lists have a PURGED state.  Purging a
mailing list on the Launchpad side performs no communication with Mailman; the
Launchpad administrator must ensure that all associated state is purged from
Mailman (which is aided by the use of a script to be run on that server).  On
Launchpad, only a Launchpad administrator or mailing list expert may purge a
list, and then only if the list is already in one of the safe-to-purge states.

A list in the active state is not safe to purge.

    >>> print list_six.status.name
    ACTIVE
    >>> list_six.purge()
    Traceback (most recent call last):
    UnsafeToPurge: Cannot purge mailing list in ACTIVE state: team-canonical

By deactivating the mailing list, we make it safe to purge.

    >>> # Need to commit, or security checks fail because team isn't yet
    >>> # available via the auth Store yet.
    >>> import transaction
    >>> transaction.commit()
    >>> list_six.deactivate()
    >>> list_six.transitionToStatus(MailingListStatus.INACTIVE)
    >>> print list_six.status.name
    INACTIVE
    >>> list_six.purge()
    >>> print list_six.status.name
    PURGED

It's as if the mailing list never existed, so we can re-request that the list
be created.

    >>> list_six = list_set.new(team_six)
    >>> print list_six.team.name
    team-canonical
    >>> print list_six.date_activated
    None
    >>> print list_six.status.name
    APPROVED
    >>> print list_six.welcome_message
    None

A list that has been approved, or is being constructed cannot be purged.

    >>> import transaction
    >>> from zope.security.proxy import removeSecurityProxy
    >>> naked_list = removeSecurityProxy(list_six)
    >>> naked_list.status = MailingListStatus.APPROVED
    >>> transaction.commit()
    >>> login(ANONYMOUS)
    >>> print list_six.status.name
    APPROVED
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in APPROVED state: team-canonical

    >>> naked_list.status = MailingListStatus.CONSTRUCTING
    >>> transaction.commit()
    >>> print list_six.status.name
    CONSTRUCTING
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in CONSTRUCTING state: ...

A list in the FAILED state can be purged, but a list in the MOD_FAILED state
cannot.  This is because the latter still means that a mailing list is active
for the team.

    >>> naked_list.status = MailingListStatus.FAILED
    >>> transaction.commit()
    >>> print list_six.status.name
    FAILED
    >>> list_six.purge()
    >>> print list_six.status.name
    PURGED

    >>> naked_list.status = MailingListStatus.MOD_FAILED
    >>> transaction.commit()
    >>> print list_six.status.name
    MOD_FAILED
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in MOD_FAILED state: ...

Modified, updating, and deactivating mailing lists are also unsafe to purge.

    >>> naked_list.status = MailingListStatus.MODIFIED
    >>> transaction.commit()
    >>> print list_six.status.name
    MODIFIED
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in MODIFIED state: team-canonical

    >>> naked_list.status = MailingListStatus.UPDATING
    >>> transaction.commit()
    >>> print list_six.status.name
    UPDATING
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in UPDATING state: team-canonical

    >>> naked_list.status = MailingListStatus.DEACTIVATING
    >>> transaction.commit()
    >>> print list_six.status.name
    DEACTIVATING
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    UnsafeToPurge: Cannot purge mailing list in DEACTIVATING state: ...

You should never be able to purge an already purged mailing list.

    >>> naked_list.status = MailingListStatus.PURGED
    >>> transaction.commit()
    >>> print list_six.status.name
    PURGED
    >>> list_six.purge()
    Traceback (most recent call last):
    ...
    AssertionError: Already purged