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
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
|
BugSubscription
===============
Users can get email notifications of changes to bugs by subscribing to
them.
Bug Subscriber APIs
-------------------
First, let's login:
>>> from lp.testing import login
>>> login("foo.bar@canonical.com")
IBug has a subscriptions attribute:
>>> from zope.component import getUtility
>>> from lp.bugs.interfaces.bug import IBugSet
>>> bugset = getUtility(IBugSet)
>>> bug = bugset.get(1)
>>> bug.subscriptions.count()
2
This list returns only *direct* subscribers. Bugs can also have
indirect subscribers.
Direct vs. Indirect Subscriptions
.................................
A user is directly subscribed to a bug if they or someone else has
subscribed them to the bug.
Then there are three kinds of users that are indirectly subscribed to
a bug:
* assignees
* structural subscribers (subscribers to the bug's target)
* direct subscribers from dupes
Bugs may get reassigned, bug subscribers may come and go, and dupes may
be unduped or reduped to other bugs. Indirect subscriptions are looked
up at mail sending time, so the mail is automatically sent to new bug
subscribers or assignees, stops being sent to subscribers from dupes when
a bug is unduped, and so forth.
Let's create a new bug to demonstrate how direct and indirect
subscriptions work.
>>> from lp.services.webapp.interfaces import ILaunchBag
>>> from lp.bugs.interfaces.bug import CreateBugParams
>>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.registry.interfaces.person import IPersonSet
>>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
>>> personset = getUtility(IPersonSet)
>>> linux_source = ubuntu.getSourcePackage("linux-source-2.6.15")
>>> list(linux_source.bug_subscriptions)
[]
>>> print linux_source.distribution.bug_supervisor
None
>>> foobar = getUtility(ILaunchBag).user
>>> print foobar.name
name16
>>> params = CreateBugParams(
... title="a bug to test subscriptions",
... comment="test", owner=foobar)
>>> linux_source_bug = linux_source.createBug(params)
The list of direct bug subscribers is accessed via
IBug.getDirectSubscribers().
>>> def print_displayname(subscribers):
... subscriber_names = sorted(subscriber.displayname
... for subscriber in subscribers)
... for name in subscriber_names:
... print name
>>> print_displayname(linux_source_bug.getDirectSubscribers())
Foo Bar
>>> mark = personset.getByName("mark")
>>> linux_source_bug.subscribe(mark, mark)
<lp.bugs.model.bugsubscription.BugSubscription ...>
>>> print_displayname(linux_source_bug.getDirectSubscribers())
Foo Bar
Mark Shuttleworth
The list of indirect subscribers is accessed via
IBug.getIndirectSubscribers().
>>> linux_source_bug.getIndirectSubscribers()
[<Person at ...>]
Finer-grained access to indirect subscribers is provided by
getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates().
>>> list(linux_source_bug.getAlsoNotifiedSubscribers())
[<Person at ...>]
>>> list(linux_source_bug.getSubscribersFromDuplicates())
[]
It is also possible to get the list of indirect subscribers for an
individual bug task.
>>> from lp.bugs.model.bug import get_also_notified_subscribers
>>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
>>> list(res)
[<Person at ...>]
These are security proxied.
>>> from zope.security. proxy import Proxy
>>> isinstance(res, Proxy)
True
The list of all bug subscribers can also be accessed via
IBugTask.bug_subscribers. Our event handling machinery compares a
"snapshot" of this value, before a bug was changed, to the current
value, to check if there are new bugcontacts subscribed to this bug as a
result of a product or sourcepackage reassignment. It's also an
optimization to snapshot this list only on IBugTask, because we don't
need it for changes made only to IBug.
>>> task = linux_source_bug.bugtasks[0]
>>> print_displayname(task.bug_subscribers)
Foo Bar
Mark Shuttleworth
Ubuntu Team
Here are some examples of the three types of indirect subscribers:
1. Assignees
>>> sample_person = personset.getByName("name12")
>>> linux_source_bug.bugtasks[0].transitionToAssignee(sample_person)
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
Sample Person
Ubuntu Team
>>> linux_source_bug.getSubscribersFromDuplicates()
()
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
Sample Person
Ubuntu Team
2. Structural subscribers
>>> mr_no_privs = personset.getByName("no-priv")
>>> subscription_no_priv = linux_source.addBugSubscription(
... mr_no_privs, mr_no_privs)
>>> transaction.commit()
>>> print_displayname(
... sub.subscriber for sub in linux_source.bug_subscriptions)
No Privileges Person
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Sample Person
Ubuntu Team
>>> linux_source_bug.getSubscribersFromDuplicates()
()
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
No Privileges Person
Sample Person
Ubuntu Team
>>> ubuntu_team = personset.getByName("ubuntu-team")
>>> linux_source.distribution.setBugSupervisor(ubuntu_team, ubuntu_team)
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
No Privileges Person
Sample Person
Ubuntu Team
After adding a product bugtask we can see that the upstream bug
supervisor is also an indirect subscriber.
>>> from lp.bugs.interfaces.bugtask import IBugTaskSet
>>> from lp.registry.interfaces.product import IProductSet
>>> firefox = getUtility(IProductSet).get(4)
>>> getUtility(IBugTaskSet).createTask(linux_source_bug, foobar, firefox)
<BugTask ...>
>>> lifeless = personset.getByName("lifeless")
>>> firefox.setBugSupervisor(lifeless, lifeless)
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
If there were no upstream product bug subscribers, the product owner
would be used instead.
>>> firefox.setBugSupervisor(None, None)
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> previous_owner = firefox.owner
>>> firefox.owner = lifeless
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> firefox.owner = previous_owner
>>> firefox.setBugSupervisor(lifeless, lifeless)
IBug.getAlsoNotifiedSubscribers() and IBug.getIndirectSubscribers() take
an optional parameter `level` allowing us to filter the result by
BugNotificationLevel for structural subscriptions. Only subscribers who
have a bug notification level greater than or equal to the value passed
in the `level` parameter are returned.
Structural subscriptions control their bug notification levels via one
or more filters. If there are no explicit filters, the default subscription
filter is interpreted to mean that the subscriber wants all notifications.
In the case of bug notification levels, that is equivalent to
BugNotificationLevel.COMMENTS.
>>> print subscription_no_priv.bug_filters.count()
1
With this subscription level, No Privileges Person is returned for all
parameter values of level.
>>> from lp.bugs.enum import BugNotificationLevel
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers(
... level=BugNotificationLevel.COMMENTS))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getIndirectSubscribers(
... level=BugNotificationLevel.COMMENTS))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers(
... level=BugNotificationLevel.LIFECYCLE))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getIndirectSubscribers(
... level=BugNotificationLevel.LIFECYCLE))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
If No Privileges Person created a single filter with a notification
level set to LIFECYCLE, he will not be included, if the parameter
`level` is METADATA or COMMENTS.
>>> from lp.testing import person_logged_in
>>> with person_logged_in(mr_no_privs):
... filter_no_priv = subscription_no_priv.bug_filters.one()
... filter_no_priv.bug_notification_level = (
... BugNotificationLevel.LIFECYCLE)
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers(
... level=BugNotificationLevel.LIFECYCLE))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getIndirectSubscribers(
... level=BugNotificationLevel.LIFECYCLE))
No Privileges Person
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers(
... level=BugNotificationLevel.METADATA))
Robert Collins
Sample Person
Ubuntu Team
>>> print_displayname(linux_source_bug.getIndirectSubscribers(
... level=BugNotificationLevel.METADATA))
Robert Collins
Sample Person
Ubuntu Team
3. Direct subscribers of duplicate bugs.
>>> keybuk = personset.getByName("keybuk")
>>> params = CreateBugParams(
... title="a bug to test subscriptions",
... comment="test", owner=keybuk)
>>> linux_source_bug_dupe = linux_source.createBug(params)
>>> print_displayname(linux_source_bug_dupe.getDirectSubscribers())
Scott James Remnant
Indirect subscribers of duplicates are *not* subscribed to dupe
targets. For example, assigning stub to the dupe bug will demonstrate
how he, as an indirect subscriber of the dupe, but does not get
subscribed to the dupe target.
>>> linux_source_bug_dupe.bugtasks[0].transitionToAssignee(
... personset.getByName("stub"))
>>> print_displayname(linux_source_bug_dupe.getIndirectSubscribers())
No Privileges Person
Stuart Bishop
Ubuntu Team
>>> linux_source_bug_dupe.markAsDuplicate(linux_source_bug)
>>> linux_source_bug_dupe.syncUpdate()
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
No Privileges Person
Robert Collins
Sample Person
Scott James Remnant
Ubuntu Team
>>> print_displayname(linux_source_bug.getSubscribersFromDuplicates())
Scott James Remnant
If Scott James Remnant makes a structural subscription to linux_source,
he will no longer appear in the list of subscribers of the duplicate
bug.
>>> subscription_keybuk = linux_source.addBugSubscription(
... keybuk, keybuk)
>>> linux_source_bug.getSubscribersFromDuplicates()
()
When a bug is marked private, specific people like the bugtask bug supervisors
will be automatically subscribed, and only specifically allowed existing
direct subscribers (eg bugtask pillar maintainers) will remain subscribed.
We currently use a feature flag to control who is subscribed when a bug is
made private and to allow multi-pillar bugs to be private.
>>> from lp.services.features.testing import FeatureFixture
>>> feature_flag = {
... 'disclosure.allow_multipillar_private_bugs.enabled': 'on',
... 'disclosure.enhanced_private_bug_subscriptions.enabled': 'on'}
>>> flags = FeatureFixture(feature_flag)
>>> flags.setUp()
>>> from zope.event import notify
>>> from lazr.lifecycle.event import ObjectModifiedEvent
>>> from lazr.lifecycle.snapshot import Snapshot
>>> from lp.bugs.interfaces.bug import IBug
>>> print_displayname(linux_source_bug.getDirectSubscribers())
Foo Bar
Mark Shuttleworth
>>> bug_before_modification = Snapshot(linux_source_bug, providing=IBug)
>>> linux_source_bug.setPrivate(True, getUtility(ILaunchBag).user)
True
>>> notify(
... ObjectModifiedEvent(
... linux_source_bug, bug_before_modification, ["private"]))
>>> print_displayname(linux_source_bug.getDirectSubscribers())
Foo Bar
Mark Shuttleworth
Robert Collins
Ubuntu Team
A private bug never has indirect subscribers. Let's add an indirect subscriber
to show that they still aren't included in the indirect subscriptions.
>>> linux_source_bug.bugtasks[0].transitionToAssignee(
... personset.getByName("martin-pitt"))
>>> linux_source_bug.getIndirectSubscribers()
[]
>>> linux_source_bug.getSubscribersFromDuplicates()
()
Direct subscriptions always take precedence over indirect subscriptions.
So, if we unmark the above bug as private, indirect_subscribers will include
any subscribers not converted to direct subscribers.
>>> linux_source_bug.setPrivate(False, getUtility(ILaunchBag).user)
True
>>> linux_source_bug.syncUpdate()
>>> print_displayname(linux_source_bug.getDirectSubscribers())
Foo Bar
Mark Shuttleworth
>>> print_displayname(linux_source_bug.getIndirectSubscribers())
Martin Pitt
No Privileges Person
Robert Collins
Sample Person
Scott James Remnant
Ubuntu Team
>>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
Martin Pitt
No Privileges Person
Robert Collins
Sample Person
Scott James Remnant
Ubuntu Team
Clean up the feature flag.
>>> flags.cleanUp()
To find out which email addresses should receive a notification email on
a bug, and why, IBug.getBugNotificationRecipients() assembles an
INotificationRecipientSet instance for us:
>>> recipients = linux_source_bug.getBugNotificationRecipients()
You can query for the addresses and reasons:
>>> addresses = recipients.getEmails()
>>> [(address, recipients.getReason(address)[1]) for address in addresses]
[('foo.bar@canonical.com', 'Subscriber'),
('mark@example.com', 'Subscriber'),
('no-priv@canonical.com', u'Subscriber (linux-source-2.6.15 in Ubuntu)'),
('robertc@robertcollins.net', u'Subscriber (Mozilla Firefox)'),
('support@ubuntu.com', u'Subscriber (Ubuntu) @ubuntu-team'),
('test@canonical.com', 'Assignee')]
If IBug.getBugNotificationRecipients() is passed a BugNotificationLevel
in its `level` parameter, only structural subscribers with that
notification level or higher will be returned.
>>> recipients = linux_source_bug.getBugNotificationRecipients(
... level=BugNotificationLevel.COMMENTS)
>>> addresses = recipients.getEmails()
>>> [(address, recipients.getReason(address)[1]) for address in addresses]
[('foo.bar@canonical.com', 'Subscriber'),
('mark@example.com', 'Subscriber'),
('robertc@robertcollins.net', u'Subscriber (Mozilla Firefox)'),
('support@ubuntu.com', u'Subscriber (Ubuntu) @ubuntu-team'),
('test@canonical.com', 'Assignee')]
When Sample Person is unsubscribed from linux_source_bug, he is no
longer included in the result of getBugNotificationRecipients() for
the COMMENTS level...
>>> linux_source_bug.unsubscribe(mr_no_privs, mr_no_privs)
>>> recipients = linux_source_bug.getBugNotificationRecipients(
... level=BugNotificationLevel.COMMENTS)
>>> addresses = recipients.getEmails()
>>> [(address, recipients.getReason(address)[1]) for address in addresses]
[('foo.bar@canonical.com', 'Subscriber'),
('mark@example.com', 'Subscriber'),
('robertc@robertcollins.net', u'Subscriber (Mozilla Firefox)'),
('support@ubuntu.com', u'Subscriber (Ubuntu) @ubuntu-team'),
('test@canonical.com', 'Assignee')]
...but remains included for the level LIFECYCLE.
>>> linux_source_bug.unsubscribe(mr_no_privs, mr_no_privs)
>>> recipients = linux_source_bug.getBugNotificationRecipients(
... level=BugNotificationLevel.LIFECYCLE)
>>> addresses = recipients.getEmails()
>>> [(address, recipients.getReason(address)[1]) for address in addresses]
[('foo.bar@canonical.com', 'Subscriber'),
('mark@example.com', 'Subscriber'),
('no-priv@canonical.com', u'Subscriber (linux-source-2.6.15 in Ubuntu)'),
('robertc@robertcollins.net', u'Subscriber (Mozilla Firefox)'),
('support@ubuntu.com', u'Subscriber (Ubuntu) @ubuntu-team'),
('test@canonical.com', 'Assignee')]
To find out if someone is already directly subscribed to a bug, call
IBug.isSubscribed, passing in an IPerson:
>>> linux_source_bug.isSubscribed(personset.getByName("debonzi"))
False
>>> name16 = personset.getByName("name16")
>>> linux_source_bug.isSubscribed(name16)
True
Call isSubscribedToDupes to see if a user is directly subscribed to
dupes of a bug. This is useful for, for example, figuring out how to
display the Subscribe/Unsubscribe menu option, and in TAL, for deciding
whether the user needs to be warned, while unsubscribing, that they will
be unsubscribed from dupes.
>>> bug_five = bugset.get(5)
>>> bug_six = bugset.get(6)
>>> bug_six.duplicateof == bug_five
True
>>> bug_five.isSubscribedToDupes(sample_person)
False
>>> bug_six.subscribe(sample_person, sample_person)
<lp.bugs.model.bugsubscription.BugSubscription...>
>>> bug_five.isSubscribedToDupes(sample_person)
True
Subscribing and Unsubscribing
-----------------------------
To subscribe people to and unsubscribe people from a bug, use
IBug.subscribe and IBug.unsubscribe:
>>> foobar = personset.getByName("name16")
>>> bug.isSubscribed(foobar)
False
>>> subscription = bug.subscribe(foobar, foobar)
>>> bug.isSubscribed(foobar)
True
>>> bug.unsubscribe(foobar, foobar)
>>> bug.isSubscribed(foobar)
False
By default, the bug_notification_level of the new subscription will be
COMMENTS, so the user will receive all notifications about the bug.
>>> print subscription.bug_notification_level.name
COMMENTS
It's possible to subscribe to a bug at a different BugNotificationLevel
by passing a `level` parameter to subscribe().
>>> metadata_subscriber = factory.makePerson()
>>> metadata_subscribed_bug = factory.makeBug()
>>> metadata_subscription = metadata_subscribed_bug.subscribe(
... metadata_subscriber, metadata_subscriber,
... level=BugNotificationLevel.METADATA)
>>> print metadata_subscription.bug_notification_level.name
METADATA
To unsubscribe from all dupes for a bug, call
IBug.unsubscribeFromDupes. This is useful because direct subscribers
from dupes are automatically subscribed to dupe targets, so we provide
them a way to unsubscribe.
For example, Sample Person can be unsubscribed from bug #6, by
unsubscribing them from the dupes of bug #5, because bug #6 is a dupe of
bug #5.
>>> bug_six.duplicateof == bug_five
True
>>> bug_six.isSubscribed(sample_person)
True
The return value of unsubscribeFromDupes() is a list of bugs from which
the user was unsubscribed.
>>> [bug.id for bug in bug_five.unsubscribeFromDupes(
... sample_person, sample_person)]
[6]
>>> bug_six.isSubscribed(sample_person)
False
Determining whether a user can unsubscribe someone
..................................................
As user can't unsubscribe just anyone from a bug. To check whether
someone can be unusubscribed, the canBeUnsubscribedByUser() method on
the BugSubscription object is used.
The user can of course unsubscribe themselves, even if someone else
subscribed them.
>>> bug = factory.makeBug()
>>> subscriber = factory.makePerson()
>>> subscribed_by = factory.makePerson()
>>> subscription = bug.subscribe(subscriber, subscribed_by)
>>> subscription.canBeUnsubscribedByUser(subscriber)
True
The one who subscribed the subscriber does have permission to
unsubscribe them.
>>> subscription.canBeUnsubscribedByUser(subscribed_by)
True
Launchpad administrators can also unsubscribe them.
>>> subscription.canBeUnsubscribedByUser(foobar)
True
The anonymous user (represented by None) also can't unsubscribe them.
>>> subscription.canBeUnsubscribedByUser(None)
False
A user can unsubscribe a team he's a member of.
>>> team = factory.makeTeam()
>>> member = factory.makePerson()
>>> member.join(team)
>>> subscription = bug.subscribe(team, subscribed_by)
>>> subscription.canBeUnsubscribedByUser(member)
True
>>> non_member = factory.makePerson()
>>> subscription.canBeUnsubscribedByUser(non_member)
False
The anonymous user (represented by None) also can't unsubscribe the team.
>>> subscription.canBeUnsubscribedByUser(None)
False
A bug's unsubscribe method uses canBeUnsubscribedByUser to check
that the unsubscribing user has the appropriate permissions. unsubscribe
will raise an exception if the user does not have permission.
>>> bug.unsubscribe(team, non_member)
Traceback (most recent call last):
...
UserCannotUnsubscribePerson: ...
Automatic Subscriptions on Bug Creation
---------------------------------------
When a new bug is opened, only the bug reporter is automatically, explicitly
subscribed to the bug:
Define a function that get subscriber email addresses back conveniently:
>>> def getSubscribers(bug):
... recipients = bug.getBugNotificationRecipients()
... return recipients.getEmails()
Let's have a look at an example for a distribution bug:
>>> ubuntu.setBugSupervisor(sample_person, sample_person)
>>> params = CreateBugParams(
... title="a test bug", comment="a test description",
... owner=foobar)
>>> new_bug = ubuntu.createBug(params)
Only the bug reporter, Foo Bar, has an explicit subscription.
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
But because Sample Person is the distribution contact for Ubuntu, he
will be implicitly added to the notification recipients.
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
The distro contact will also be subscribed to private bugs, because
there is no security contact:
>>> ubuntu.security_contact is None
True
>>> from lp.services.mail import stub
>>> transaction.commit()
>>> stub.test_emails = []
>>> params = CreateBugParams(
... title="a test bug", comment="a test description",
... owner=foobar, security_related=True, private=True)
>>> new_bug = ubuntu.createBug(params)
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com']
Even though support@ubuntu.com got subscribed while filing the bug, no
"You have been subscribed" notification was sent, which is normally sent
to new subscribers.
>>> transaction.commit()
>>> stub.test_emails
[]
Another example, this time for an upstream:
>>> firefox.setBugSupervisor(mark, mark)
>>> params = CreateBugParams(
... title="a test bug", comment="a test description",
... owner=foobar)
>>> new_bug = firefox.createBug(params)
Again, only Foo Bar is explicitly subscribed:
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
But the upstream Firefox bug supervisor, mark, is implicitly added to the
recipients list.
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'mark@example.com', 'robertc@robertcollins.net']
If we create a bug task on Ubuntu in the same bug, the Ubuntu bug
supervisor will be subscribed:
>>> ubuntu_task = getUtility(IBugTaskSet).createTask(
... new_bug, mark, ubuntu)
>>> print '\n'.join(getSubscribers(new_bug))
foo.bar@canonical.com
mark@example.com
robertc@robertcollins.net
support@ubuntu.com
test@canonical.com
But still, only Foo Bar is explicitly subscribed.
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
When an upstream does *not* have a specific bug supervisor set, the
product.owner is used instead. So, if Firefox's bug supervisor is unset,
Sample Person, the Firefox "owner" will get subscribed instead:
>>> firefox.setBugSupervisor(None, None)
>>> params = CreateBugParams(
... title="a test bug", comment="a test description",
... owner=foobar)
>>> new_bug = firefox.createBug(params)
Foo Bar is the only explicit subscriber:
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
But the product owner, Sample Person, is implicitly added to the
recipient list:
>>> print '\n'.join(getSubscribers(new_bug))
foo.bar@canonical.com
mark@example.com
robertc@robertcollins.net
test@canonical.com
The upstream maintainer will be subscribed to security-related private
bugs, because upstream has no security contact, in this case.
>>> firefox.security_contact is None
True
>>> params = CreateBugParams(
... title="a test bug", comment="a test description",
... owner=foobar, security_related=True, private=True)
>>> new_bug = firefox.createBug(params)
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'test@canonical.com']
Now let's create a bug on a specific package, which has no package bug
contacts:
>>> evolution = ubuntu.getSourcePackage("evolution")
>>> list(evolution.bug_subscriptions)
[]
>>> params = CreateBugParams(
... title="another test bug",
... comment="another test description",
... owner=foobar)
>>> new_bug = evolution.createBug(params)
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
Adding a package bug contact for evolution will mean that that package
bug contact gets implicitly subscribed to all bugs ever opened on that
package.
So, if the Ubuntu team is added as a bug contact to evolution:
>>> evolution.addBugSubscription(ubuntu_team, ubuntu_team)
<...StructuralSubscription object at ...>
The team will be implicitly subscribed to the previous bug we
created. (Remember that Sample Person is also implicitly subscribed
because they are the distro bug contact):
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
And the Ubuntu team will be implicitly subscribed to future bugs:
>>> params = CreateBugParams(
... title="yet another test bug",
... comment="yet another test description",
... owner=foobar)
>>> new_bug = evolution.createBug(params)
>>> [subscription.person.displayname
... for subscription in new_bug.subscriptions]
[u'Foo Bar']
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
The distribution maintainer, Ubuntu Team, gets subscribed to the private
security bug filed on a package, because Ubuntu has no security contact:
>>> ubuntu.security_contact is None
True
>>> params = CreateBugParams(
... title="yet another test bug",
... comment="yet another test description",
... owner=foobar, security_related=True, private=True)
>>> new_bug = evolution.createBug(params)
>>> getSubscribers(new_bug)
['foo.bar@canonical.com', 'support@ubuntu.com']
Subscribed by
-------------
Each `BugSubscription` records who created it, and provides a handy
utility method for formatting this information. The methods
`getDirectSubscriptions` and `getSubscriptionsFromDuplicates` provide
an equivalent to the -Subscribers methods, but returning the
subscriptions themselves, rather than the subscribers.
>>> params = CreateBugParams(
... title="one more test bug",
... comment="one more test description",
... owner=mark)
>>> ff_bug = firefox.createBug(params)
>>> ff_bug.subscribe(lifeless, mark)
<lp.bugs.model.bugsubscription.BugSubscription ...>
>>> subscriptions = [
... "%s: %s" % (
... subscription.person.displayname,
... subscription.display_subscribed_by)
... for subscription in ff_bug.getDirectSubscriptions()]
>>> for subscription in sorted(subscriptions):
... print subscription
Mark Shuttleworth: Self-subscribed
Robert Collins: Subscribed by Mark Shuttleworth (mark)
>>> params = CreateBugParams(
... title="one more dupe test bug",
... comment="one more dupe test description",
... owner=keybuk)
>>> dupe_ff_bug = firefox.createBug(params)
>>> dupe_ff_bug.markAsDuplicate(ff_bug)
>>> dupe_ff_bug.syncUpdate()
>>> dupe_ff_bug.subscribe(foobar, lifeless)
<lp.bugs.model.bugsubscription.BugSubscription ...>
>>> for subscription in ff_bug.getSubscriptionsFromDuplicates():
... print '%s: %s' % (
... subscription.person.displayname,
... subscription.display_duplicate_subscribed_by)
Scott James Remnant: Self-subscribed to bug ...
Foo Bar: Subscribed to bug ... by Robert Collins (lifeless)
|