Merge lp:~stevenk/launchpad/structsub-private-bugs-redux into lp:launchpad
- structsub-private-bugs-redux
- Merge into devel
Proposed by
Steve Kowalik
Status: | Merged |
---|---|
Approved by: | William Grant |
Approved revision: | no longer in the source branch. |
Merged at revision: | 15720 |
Proposed branch: | lp:~stevenk/launchpad/structsub-private-bugs-redux |
Merge into: | lp:launchpad |
Diff against target: |
840 lines (+173/-263) 16 files modified
database/schema/security.cfg (+2/-0) lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt (+3/-18) lib/lp/bugs/doc/bug.txt (+5/-12) lib/lp/bugs/doc/bugsubscription.txt (+5/-16) lib/lp/bugs/interfaces/bug.py (+1/-4) lib/lp/bugs/mail/bugnotificationrecipients.py (+1/-1) lib/lp/bugs/model/bug.py (+54/-51) lib/lp/bugs/model/structuralsubscription.py (+8/-0) lib/lp/bugs/model/tests/test_bug.py (+0/-19) lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+1/-40) lib/lp/bugs/scripts/bugnotification.py (+1/-2) lib/lp/bugs/subscribers/bug.py (+7/-10) lib/lp/bugs/tests/test_bug_notification_recipients.py (+75/-0) lib/lp/bugs/tests/test_bugchanges.py (+0/-78) lib/lp/registry/model/accesspolicy.py (+1/-1) lib/lp/registry/tests/test_sharingjob.py (+9/-11) |
To merge this branch: | bzr merge lp:~stevenk/launchpad/structsub-private-bugs-redux |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+116798@code.launchpad.net |
Commit message
Filter direct (in one case), indirect, structural and duplicate subscriptions on bugs by those who have an AAG or APG.
Description of the change
Filter indirect, structural and duplicate subscriptions by those who have an AAG or APG. Remove include_
I have removed a whole swatch of tests for being completely pointless with this change.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2012-07-25 18:56:53 +0000 |
3 | +++ database/schema/security.cfg 2012-07-31 01:59:19 +0000 |
4 | @@ -1513,8 +1513,10 @@ |
5 | [bugnotification] |
6 | groups=script |
7 | public.accessartifact = SELECT, INSERT, DELETE |
8 | +public.accessartifactgrant = SELECT |
9 | public.accesspolicy = SELECT |
10 | public.accesspolicyartifact = SELECT, INSERT, DELETE |
11 | +public.accesspolicygrant = SELECT |
12 | public.account = SELECT |
13 | public.answercontact = SELECT |
14 | public.archive = SELECT |
15 | |
16 | === modified file 'lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt' |
17 | --- lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt 2012-07-27 01:15:04 +0000 |
18 | +++ lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt 2012-07-31 01:59:19 +0000 |
19 | @@ -387,17 +387,6 @@ |
20 | >>> filebug_view.added_bug.information_type |
21 | <DBItem InformationType.USERDATA, (4) Private> |
22 | |
23 | -Since the bug was marked private before it was filed, only the bug |
24 | -reporter has been subscribed to the bug and there should be no indirect |
25 | -subscribers. |
26 | - |
27 | - >>> for subscriber in filebug_view.added_bug.getDirectSubscribers(): |
28 | - ... print subscriber.displayname |
29 | - No Privileges Person |
30 | - |
31 | - >>> filebug_view.added_bug.getIndirectSubscribers() |
32 | - [] |
33 | - |
34 | The user will be notified that the bug has been marked as private. |
35 | |
36 | >>> print filebug_view.request.response.notifications[2].message |
37 | @@ -594,11 +583,6 @@ |
38 | Mark Shuttleworth |
39 | No Privileges Person |
40 | |
41 | -Since the bug is private, there should be no indirect subscribers. |
42 | - |
43 | - >>> filebug_view.added_bug.getIndirectSubscribers() |
44 | - [] |
45 | - |
46 | The user will be notified that Mark Shuttleworth has been subscribed to |
47 | this bug and that the bug has been marked as private. |
48 | |
49 | @@ -718,8 +702,9 @@ |
50 | >>> product.setBugSupervisor(owner, owner) |
51 | >>> supervisor_fields = set(filebug_view.field_names) |
52 | |
53 | -Privileged users get most of the same fields as normal users, plus a few extra. |
54 | -The security_related checkbox is replaced by an information_type radio group. |
55 | +Privileged users get most of the same fields as normal users, plus a few |
56 | +extra. The security_related checkbox is replaced by an information_type |
57 | +radio group. |
58 | |
59 | >>> normal_fields.remove('security_related') |
60 | >>> owner_fields == supervisor_fields |
61 | |
62 | === modified file 'lib/lp/bugs/doc/bug.txt' |
63 | --- lib/lp/bugs/doc/bug.txt 2012-07-19 04:40:03 +0000 |
64 | +++ lib/lp/bugs/doc/bug.txt 2012-07-31 01:59:19 +0000 |
65 | @@ -169,7 +169,7 @@ |
66 | private. A bug cannot be made private by an anonymous user. |
67 | |
68 | >>> from lp.services.webapp.interfaces import ILaunchBag |
69 | - |
70 | + |
71 | >>> def current_user(): |
72 | ... return getUtility(ILaunchBag).user |
73 | |
74 | @@ -423,11 +423,6 @@ |
75 | >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()] |
76 | [u'name16'] |
77 | |
78 | -Since it's private, there are no indirect subscribers. |
79 | - |
80 | - >>> private_bug.getIndirectSubscribers() |
81 | - [] |
82 | - |
83 | It's up to the submitter to subscribe the maintainer, if they so choose. |
84 | |
85 | This works similarly for distributions; in this case the |
86 | @@ -449,8 +444,6 @@ |
87 | >>> private_bug = bugset.get(added_bug.id) |
88 | >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()] |
89 | [u'name16'] |
90 | - >>> private_bug.getIndirectSubscribers() |
91 | - [] |
92 | |
93 | |
94 | Prevent reporter from being subscribed to filed bugs |
95 | @@ -1084,10 +1077,10 @@ |
96 | The thunderbird project does not use Launchpad to track bugs. |
97 | Incomplete, unattended bug reports cannot ever expire for this project. |
98 | |
99 | - >>> # create_old_bug creates an bug with a bugtask that is eligible for |
100 | - >>> # expiration, so long as the pillar object has enabled bug expiration. |
101 | - >>> # Every change to a bug or bugtask must be synced back to the |
102 | - >>> # database to test can_expire. |
103 | +create_old_bug creates an bug with a bugtask that is eligible for expiration, |
104 | +so long as the pillar object has enabled bug expiration. Every change to a |
105 | +bug or bugtask must be synced back to the database to test can_expire. |
106 | + |
107 | >>> from lp.bugs.tests.bug import create_old_bug |
108 | |
109 | >>> upstream_bugtask = create_old_bug('bug a', 1, thunderbird) |
110 | |
111 | === modified file 'lib/lp/bugs/doc/bugsubscription.txt' |
112 | --- lib/lp/bugs/doc/bugsubscription.txt 2012-07-27 01:15:04 +0000 |
113 | +++ lib/lp/bugs/doc/bugsubscription.txt 2012-07-31 01:59:19 +0000 |
114 | @@ -400,18 +400,6 @@ |
115 | Mark Shuttleworth |
116 | Robert Collins |
117 | |
118 | -A private bug never has indirect subscribers. Let's add an indirect subscriber |
119 | -to show that they still aren't included in the indirect subscriptions. |
120 | - |
121 | - >>> linux_source_bug.bugtasks[0].transitionToAssignee( |
122 | - ... personset.getByName("martin-pitt")) |
123 | - |
124 | - >>> linux_source_bug.getIndirectSubscribers() |
125 | - [] |
126 | - |
127 | - >>> linux_source_bug.getSubscribersFromDuplicates() |
128 | - () |
129 | - |
130 | Direct subscriptions always take precedence over indirect subscriptions. |
131 | So, if we unmark the above bug as private, indirect_subscribers will include |
132 | any subscribers not converted to direct subscribers. |
133 | @@ -425,14 +413,12 @@ |
134 | Robert Collins |
135 | |
136 | >>> print_displayname(linux_source_bug.getIndirectSubscribers()) |
137 | - Martin Pitt |
138 | No Privileges Person |
139 | Sample Person |
140 | Scott James Remnant |
141 | Ubuntu Team |
142 | |
143 | >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers()) |
144 | - Martin Pitt |
145 | No Privileges Person |
146 | Sample Person |
147 | Scott James Remnant |
148 | @@ -786,8 +772,11 @@ |
149 | ... owner=foobar, information_type=InformationType.PRIVATESECURITY) |
150 | >>> new_bug = firefox.createBug(params) |
151 | |
152 | - >>> getSubscribers(new_bug) |
153 | - ['foo.bar@canonical.com', 'test@canonical.com'] |
154 | + >>> print '\n'.join(getSubscribers(new_bug)) |
155 | + foo.bar@canonical.com |
156 | + mark@example.com |
157 | + robertc@robertcollins.net |
158 | + test@canonical.com |
159 | |
160 | Now let's create a bug on a specific package, which has no package bug |
161 | contacts: |
162 | |
163 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
164 | --- lib/lp/bugs/interfaces/bug.py 2012-07-30 00:05:44 +0000 |
165 | +++ lib/lp/bugs/interfaces/bug.py 2012-07-31 01:59:19 +0000 |
166 | @@ -518,8 +518,7 @@ |
167 | `BugSubscriptionLevel.LIFECYCLE` if unspecified. |
168 | """ |
169 | |
170 | - def getBugNotificationRecipients(duplicateof=None, old_bug=None, |
171 | - include_master_dupe_subscribers=False): |
172 | + def getBugNotificationRecipients(duplicateof=None, old_bug=None): |
173 | """Return a complete INotificationRecipientSet instance. |
174 | |
175 | The INotificationRecipientSet instance will contain details of |
176 | @@ -527,8 +526,6 @@ |
177 | includes email addresses and textual and header-ready |
178 | rationales. See `BugNotificationRecipients` for |
179 | details of this implementation. |
180 | - If this bug is a dupe, set include_master_dupe_subscribers to |
181 | - True to include the master bug's subscribers as recipients. |
182 | """ |
183 | |
184 | def canBeAQuestion(): |
185 | |
186 | === modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py' |
187 | --- lib/lp/bugs/mail/bugnotificationrecipients.py 2012-07-27 01:36:30 +0000 |
188 | +++ lib/lp/bugs/mail/bugnotificationrecipients.py 2012-07-31 01:59:19 +0000 |
189 | @@ -56,7 +56,7 @@ |
190 | duplicateof parameter above and the addDupeSubscriber method. |
191 | Don't confuse them! |
192 | """ |
193 | - NotificationRecipientSet.__init__(self) |
194 | + super(BugNotificationRecipients, self).__init__() |
195 | self.duplicateof = duplicateof |
196 | self.subscription_filters = set() |
197 | |
198 | |
199 | === modified file 'lib/lp/bugs/model/bug.py' |
200 | --- lib/lp/bugs/model/bug.py 2012-07-30 00:05:44 +0000 |
201 | +++ lib/lp/bugs/model/bug.py 2012-07-31 01:59:19 +0000 |
202 | @@ -166,8 +166,6 @@ |
203 | from lp.registry.interfaces.accesspolicy import ( |
204 | IAccessArtifactGrantSource, |
205 | IAccessArtifactSource, |
206 | - IAccessPolicyGrantSource, |
207 | - IAccessPolicySource, |
208 | ) |
209 | from lp.registry.interfaces.distribution import IDistribution |
210 | from lp.registry.interfaces.distroseries import IDistroSeries |
211 | @@ -979,7 +977,8 @@ |
212 | """See `IBug`.""" |
213 | return self.getSubscriptionInfo().direct_subscriptions |
214 | |
215 | - def getDirectSubscribers(self, recipients=None, level=None): |
216 | + def getDirectSubscribers(self, recipients=None, level=None, |
217 | + filter_visible=False): |
218 | """See `IBug`. |
219 | |
220 | The recipients argument is private and not exposed in the |
221 | @@ -991,6 +990,13 @@ |
222 | level = BugNotificationLevel.LIFECYCLE |
223 | direct_subscribers = ( |
224 | self.getSubscriptionInfo(level).direct_subscribers) |
225 | + if filter_visible: |
226 | + filtered_subscribers = IStore(Person).find(Person, |
227 | + Person.id.is_in([s.id for s in direct_subscribers]), |
228 | + self.getSubscriptionInfo().visible_recipients_filter( |
229 | + Person.id)) |
230 | + direct_subscribers = BugSubscriberSet( |
231 | + direct_subscribers.intersection(filtered_subscribers)) |
232 | if recipients is not None: |
233 | for subscriber in direct_subscribers: |
234 | recipients.addDirectSubscriber(subscriber) |
235 | @@ -1123,27 +1129,13 @@ |
236 | return get_also_notified_subscribers(self, recipients, level) |
237 | |
238 | def getBugNotificationRecipients(self, duplicateof=None, old_bug=None, |
239 | - level=None, |
240 | - include_master_dupe_subscribers=False): |
241 | + level=None): |
242 | """See `IBug`.""" |
243 | recipients = BugNotificationRecipients(duplicateof=duplicateof) |
244 | - self.getDirectSubscribers(recipients, level=level) |
245 | - if self.private: |
246 | - assert self.getIndirectSubscribers() == [], ( |
247 | - "Indirect subscribers found on private bug. " |
248 | - "A private bug should never have implicit subscribers!") |
249 | - else: |
250 | - self.getIndirectSubscribers(recipients, level=level) |
251 | - if include_master_dupe_subscribers and self.duplicateof: |
252 | - # This bug is a public duplicate of another bug, so include |
253 | - # the dupe target's subscribers in the recipient list. Note |
254 | - # that we only do this for duplicate bugs that are public; |
255 | - # changes in private bugs are not broadcast to their dupe |
256 | - # targets. |
257 | - dupe_recipients = ( |
258 | - self.duplicateof.getBugNotificationRecipients( |
259 | - duplicateof=self.duplicateof, level=level)) |
260 | - recipients.update(dupe_recipients) |
261 | + self.getDirectSubscribers( |
262 | + recipients, level=level, filter_visible=True) |
263 | + self.getIndirectSubscribers(recipients, level=level) |
264 | + |
265 | # XXX Tom Berger 2008-03-18: |
266 | # We want to look up the recipients for `old_bug` too, |
267 | # but for this to work, this code has to move out of the |
268 | @@ -2241,9 +2233,6 @@ |
269 | else: |
270 | raise ValueError('First argument must be bug or bugtask') |
271 | |
272 | - if bug.private: |
273 | - return [] |
274 | - |
275 | # Subscribers to exclude. |
276 | exclude_subscribers = frozenset().union( |
277 | info.direct_subscribers_at_all_levels, info.muted_subscribers) |
278 | @@ -2452,6 +2441,17 @@ |
279 | muted_people = Select(BugMute.person_id, BugMute.bug == self.bug) |
280 | return load_people(Person.id.is_in(muted_people)) |
281 | |
282 | + def visible_recipients_filter(self, column): |
283 | + # Circular fail :( |
284 | + from lp.bugs.model.bugtaskflat import BugTaskFlat |
285 | + from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms |
286 | + |
287 | + if self.bug.private: |
288 | + return [Or(*get_bug_privacy_filter_terms(column)), |
289 | + BugTaskFlat.bug_id == self.bug.id] |
290 | + else: |
291 | + return True |
292 | + |
293 | @cachedproperty |
294 | @freeze(BugSubscriptionSet) |
295 | def direct_subscriptions(self): |
296 | @@ -2496,21 +2496,20 @@ |
297 | def duplicate_subscriptions(self): |
298 | """Subscriptions to duplicates of the bug. |
299 | |
300 | - Excludes muted subscriptions. |
301 | + Excludes muted subscriptions, and subscribers who can not see the |
302 | + master bug. |
303 | """ |
304 | - if self.bug.private: |
305 | - return () |
306 | - else: |
307 | - return IStore(BugSubscription).find( |
308 | - BugSubscription, |
309 | - BugSubscription.bug_notification_level >= self.level, |
310 | - BugSubscription.bug_id == Bug.id, |
311 | - Bug.duplicateof == self.bug, |
312 | - Not(In( |
313 | - BugSubscription.person_id, |
314 | - Select( |
315 | - BugMute.person_id, BugMute.bug_id == Bug.id, |
316 | - tables=[BugMute])))) |
317 | + return IStore(BugSubscription).find( |
318 | + BugSubscription, |
319 | + BugSubscription.bug_notification_level >= self.level, |
320 | + BugSubscription.bug_id == Bug.id, |
321 | + Bug.duplicateof == self.bug, |
322 | + Not(In( |
323 | + BugSubscription.person_id, |
324 | + Select( |
325 | + BugMute.person_id, BugMute.bug_id == Bug.id, |
326 | + tables=[BugMute]))), |
327 | + self.visible_recipients_filter(BugSubscription.person_id)) |
328 | |
329 | @property |
330 | def duplicate_subscribers(self): |
331 | @@ -2571,22 +2570,26 @@ |
332 | *Does not* exclude muted subscribers. |
333 | """ |
334 | if self.bugtask is None: |
335 | - assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug) |
336 | - return load_people(Person.id.is_in(assignees)) |
337 | - else: |
338 | - return load_people(Person.id == self.bugtask.assigneeID) |
339 | + assignees = load_people( |
340 | + Person.id.is_in(Select(BugTask.assigneeID, |
341 | + BugTask.bug == self.bug))) |
342 | + else: |
343 | + assignees = load_people(Person.id == self.bugtask.assigneeID) |
344 | + if self.bug.private: |
345 | + return IStore(Person).find(Person, |
346 | + Person.id.is_in([a.id for a in assignees]), |
347 | + self.visible_recipients_filter(Person.id)) |
348 | + else: |
349 | + return assignees |
350 | |
351 | @cachedproperty |
352 | def also_notified_subscribers(self): |
353 | """All subscribers except direct, dupe, and muted subscribers.""" |
354 | - if self.bug.private: |
355 | - return BugSubscriberSet() |
356 | - else: |
357 | - subscribers = BugSubscriberSet().union( |
358 | - self.structural_subscribers, self.all_assignees) |
359 | - return subscribers.difference( |
360 | - self.direct_subscribers_at_all_levels, |
361 | - self.muted_subscribers) |
362 | + subscribers = BugSubscriberSet().union( |
363 | + self.structural_subscribers, self.all_assignees) |
364 | + return subscribers.difference( |
365 | + self.direct_subscribers_at_all_levels, |
366 | + self.muted_subscribers) |
367 | |
368 | @cachedproperty |
369 | def indirect_subscribers(self): |
370 | |
371 | === modified file 'lib/lp/bugs/model/structuralsubscription.py' |
372 | --- lib/lp/bugs/model/structuralsubscription.py 2012-06-14 05:18:22 +0000 |
373 | +++ lib/lp/bugs/model/structuralsubscription.py 2012-07-31 01:59:19 +0000 |
374 | @@ -702,6 +702,9 @@ |
375 | :param direct_subscribers: a collection of Person objects who are |
376 | directly subscribed to the bug. |
377 | """ |
378 | + # Circular. :-( |
379 | + from lp.bugs.model.bugtaskflat import BugTaskFlat |
380 | + from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms |
381 | # We get the ids because we need to use group by in order to |
382 | # look at the filters' tags in aggregate. Once we have the ids, |
383 | # we can get the full set of what we need in subsuming or |
384 | @@ -738,6 +741,11 @@ |
385 | Not(In(StructuralSubscription.subscriberID, |
386 | Select(BugSubscription.person_id, |
387 | BugSubscription.bug == bug)))) |
388 | + if bug.private: |
389 | + filters.extend([ |
390 | + Or(*get_bug_privacy_filter_terms( |
391 | + StructuralSubscription.subscriberID)), |
392 | + BugTaskFlat.bug == bug]) |
393 | candidates = list(_get_structural_subscriptions( |
394 | StructuralSubscription.id, query_arguments, *filters)) |
395 | if not candidates: |
396 | |
397 | === modified file 'lib/lp/bugs/model/tests/test_bug.py' |
398 | --- lib/lp/bugs/model/tests/test_bug.py 2012-07-26 05:18:32 +0000 |
399 | +++ lib/lp/bugs/model/tests/test_bug.py 2012-07-31 01:59:19 +0000 |
400 | @@ -891,25 +891,6 @@ |
401 | bug.setPrivate(True, bug.owner) |
402 | self.assertEqual(InformationType.USERDATA, bug.information_type) |
403 | |
404 | - def test_information_type_does_not_leak(self): |
405 | - # Make sure that bug notifications for private bugs do not leak to |
406 | - # people with a subscription on the product. |
407 | - product = self.factory.makeProduct() |
408 | - with person_logged_in(product.owner): |
409 | - product.addSubscription(product.owner, product.owner) |
410 | - reporter = self.factory.makePerson() |
411 | - bug = self.factory.makeBug( |
412 | - information_type=InformationType.USERDATA, product=product, |
413 | - owner=reporter) |
414 | - recipients = Store.of(bug).using( |
415 | - BugNotificationRecipient, |
416 | - Join(BugNotification, BugNotification.bugID == bug.id)).find( |
417 | - BugNotificationRecipient, |
418 | - BugNotificationRecipient.bug_notificationID == |
419 | - BugNotification.id) |
420 | - self.assertEqual( |
421 | - [reporter], [recipient.person for recipient in recipients]) |
422 | - |
423 | def test__reconcileAccess_handles_all_targets(self): |
424 | # _reconcileAccess gets the pillar from any task |
425 | # type. |
426 | |
427 | === modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py' |
428 | --- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-07-27 01:15:04 +0000 |
429 | +++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-07-31 01:59:19 +0000 |
430 | @@ -273,18 +273,6 @@ |
431 | self.assertContentEqual( |
432 | [duplicate_bug_subscription], found_subscriptions) |
433 | |
434 | - def test_duplicate_for_private_bug(self): |
435 | - # The set of subscribers from duplicate bugs is always empty when the |
436 | - # master bug is private. |
437 | - duplicate_bug = self.factory.makeBug(product=self.target) |
438 | - with person_logged_in(duplicate_bug.owner): |
439 | - duplicate_bug.markAsDuplicate(self.bug) |
440 | - with person_logged_in(self.bug.owner): |
441 | - self.bug.setPrivate(True, self.bug.owner) |
442 | - found_subscriptions = self.getInfo().duplicate_subscriptions |
443 | - self.assertContentEqual([], found_subscriptions) |
444 | - self.assertContentEqual([], found_subscriptions.subscribers) |
445 | - |
446 | def test_duplicate_only(self): |
447 | # The set of duplicate subscriptions where the subscriber has no other |
448 | # subscriptions. |
449 | @@ -430,33 +418,6 @@ |
450 | found_subscribers = self.getInfo().also_notified_subscribers |
451 | self.assertContentEqual([], found_subscribers) |
452 | |
453 | - def test_also_notified_subscribers_for_private_bug(self): |
454 | - # The set of also notified subscribers is always empty when the master |
455 | - # bug is private. |
456 | - assignee = self.factory.makePerson() |
457 | - bugtask = self.bug.default_bugtask |
458 | - with person_logged_in(bugtask.pillar.bug_supervisor): |
459 | - bugtask.transitionToAssignee(assignee) |
460 | - with person_logged_in(self.bug.owner): |
461 | - self.bug.setPrivate(True, self.bug.owner) |
462 | - found_subscribers = self.getInfo().also_notified_subscribers |
463 | - self.assertContentEqual([], found_subscribers) |
464 | - |
465 | - def test_indirect_subscribers(self): |
466 | - # The set of indirect subscribers is the union of also notified |
467 | - # subscribers and subscribers to duplicates. |
468 | - assignee = self.factory.makePerson() |
469 | - bugtask = self.bug.default_bugtask |
470 | - with person_logged_in(bugtask.pillar.bug_supervisor): |
471 | - bugtask.transitionToAssignee(assignee) |
472 | - duplicate_bug = self.factory.makeBug(product=self.target) |
473 | - with person_logged_in(duplicate_bug.owner): |
474 | - duplicate_bug.markAsDuplicate(self.bug) |
475 | - found_subscribers = self.getInfo().indirect_subscribers |
476 | - self.assertContentEqual( |
477 | - [assignee, duplicate_bug.owner], |
478 | - found_subscribers) |
479 | - |
480 | |
481 | class TestBugSubscriptionInfoPermissions(TestCaseWithFactory): |
482 | |
483 | @@ -584,7 +545,7 @@ |
484 | self.make_duplicate_bug() |
485 | with person_logged_in(self.bug.owner): |
486 | self.bug.setPrivate(True, self.bug.owner) |
487 | - with self.exactly_x_queries(0): |
488 | + with self.exactly_x_queries(1): |
489 | self.info.duplicate_subscriptions |
490 | with self.exactly_x_queries(0): |
491 | self.info.duplicate_subscriptions.subscribers |
492 | |
493 | === modified file 'lib/lp/bugs/scripts/bugnotification.py' |
494 | --- lib/lp/bugs/scripts/bugnotification.py 2012-05-08 17:04:43 +0000 |
495 | +++ lib/lp/bugs/scripts/bugnotification.py 2012-07-31 01:59:19 +0000 |
496 | @@ -313,8 +313,7 @@ |
497 | # Construct the real notification with recipients. |
498 | bug = notification.bug |
499 | recipients = bug.getBugNotificationRecipients( |
500 | - level=BugNotificationLevel.LIFECYCLE, |
501 | - include_master_dupe_subscribers=False) |
502 | + level=BugNotificationLevel.LIFECYCLE) |
503 | message = notification.message |
504 | is_comment = notification.is_comment |
505 | activity = notification.activity |
506 | |
507 | === modified file 'lib/lp/bugs/subscribers/bug.py' |
508 | --- lib/lp/bugs/subscribers/bug.py 2012-05-08 01:06:31 +0000 |
509 | +++ lib/lp/bugs/subscribers/bug.py 2012-07-31 01:59:19 +0000 |
510 | @@ -156,12 +156,11 @@ |
511 | level=BugNotificationLevel.METADATA) |
512 | recipients.update(old_bugtask_recipients) |
513 | for change in changes: |
514 | + bug = bug_delta.bug |
515 | if isinstance(change, BugDuplicateChange): |
516 | - no_dupe_master_recipients = ( |
517 | - bug_delta.bug.getBugNotificationRecipients( |
518 | - old_bug=bug_delta.bug_before_modification, |
519 | - level=change.change_level, |
520 | - include_master_dupe_subscribers=False)) |
521 | + no_dupe_master_recipients = bug.getBugNotificationRecipients( |
522 | + old_bug=bug_delta.bug_before_modification, |
523 | + level=change.change_level) |
524 | bug_delta.bug.addChange( |
525 | change, recipients=no_dupe_master_recipients) |
526 | elif (isinstance(change, BugTaskAssigneeChange) and |
527 | @@ -173,11 +172,9 @@ |
528 | bug_delta.bug.addChange(change, recipients=recipients) |
529 | else: |
530 | if change.change_level == BugNotificationLevel.LIFECYCLE: |
531 | - change_recipients = ( |
532 | - bug_delta.bug.getBugNotificationRecipients( |
533 | - old_bug=bug_delta.bug_before_modification, |
534 | - level=change.change_level, |
535 | - include_master_dupe_subscribers=False)) |
536 | + change_recipients = bug.getBugNotificationRecipients( |
537 | + old_bug=bug_delta.bug_before_modification, |
538 | + level=change.change_level) |
539 | recipients.update(change_recipients) |
540 | # Additionally, if we are re-targetting a bugtask for a private |
541 | # bug, we need to ensure the new bug supervisor and maintainer are |
542 | |
543 | === modified file 'lib/lp/bugs/tests/test_bug_notification_recipients.py' |
544 | --- lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-07-24 05:43:46 +0000 |
545 | +++ lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-07-31 01:59:19 +0000 |
546 | @@ -5,7 +5,13 @@ |
547 | |
548 | __metaclass__ = type |
549 | |
550 | +from zope.component import getUtility |
551 | + |
552 | from lp.registry.enums import InformationType |
553 | +from lp.registry.interfaces.accesspolicy import ( |
554 | + IAccessArtifactGrantSource, |
555 | + IAccessPolicySource, |
556 | + ) |
557 | from lp.testing import ( |
558 | person_logged_in, |
559 | TestCaseWithFactory, |
560 | @@ -59,6 +65,7 @@ |
561 | bug.getBugNotificationRecipients()) |
562 | |
563 | def test_private_bug(self): |
564 | + # Only the owner is notified about a private bug. |
565 | owner = self.factory.makePerson() |
566 | bug = self.factory.makeBug( |
567 | owner=owner, information_type=InformationType.USERDATA) |
568 | @@ -67,6 +74,7 @@ |
569 | [owner], bug.getBugNotificationRecipients()) |
570 | |
571 | def test_private_bug_with_subscriber(self): |
572 | + # Subscribing a user to a bug grants access, so they will be notified. |
573 | owner = self.factory.makePerson() |
574 | subscriber = self.factory.makePerson() |
575 | bug = self.factory.makeBug( |
576 | @@ -76,7 +84,23 @@ |
577 | self.assertContentEqual( |
578 | [owner, subscriber], bug.getBugNotificationRecipients()) |
579 | |
580 | + def test_private_bug_with_subscriber_without_access(self): |
581 | + # A subscriber without access to a private bug isn't notified. |
582 | + owner = self.factory.makePerson() |
583 | + subscriber = self.factory.makePerson() |
584 | + bug = self.factory.makeBug( |
585 | + owner=owner, information_type=InformationType.USERDATA) |
586 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
587 | + with person_logged_in(owner): |
588 | + bug.subscribe(subscriber, owner) |
589 | + getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
590 | + [artifact], [subscriber]) |
591 | + self.assertContentEqual( |
592 | + [owner], bug.getBugNotificationRecipients()) |
593 | + |
594 | def test_private_bug_with_structural_subscriber(self): |
595 | + # A structural subscriber without access does not get notified about |
596 | + # a private bug. |
597 | owner = self.factory.makePerson() |
598 | subscriber = self.factory.makePerson() |
599 | product = self.factory.makeProduct() |
600 | @@ -89,7 +113,26 @@ |
601 | self.assertContentEqual( |
602 | [owner], bug.getBugNotificationRecipients()) |
603 | |
604 | + def test_private_bug_with_structural_subscriber_with_access(self): |
605 | + # When a structural subscriber has access to a private bug, they are |
606 | + # notified. |
607 | + owner = self.factory.makePerson() |
608 | + subscriber = self.factory.makePerson() |
609 | + product = self.factory.makeProduct() |
610 | + with person_logged_in(subscriber): |
611 | + product.addBugSubscription(subscriber, subscriber) |
612 | + policy = getUtility(IAccessPolicySource).find( |
613 | + [(product, InformationType.USERDATA)]).one() |
614 | + self.factory.makeAccessPolicyGrant(policy=policy, grantee=subscriber) |
615 | + bug = self.factory.makeBug( |
616 | + product=product, owner=owner, |
617 | + information_type=InformationType.USERDATA) |
618 | + with person_logged_in(owner): |
619 | + self.assertContentEqual( |
620 | + [owner, subscriber], bug.getBugNotificationRecipients()) |
621 | + |
622 | def test_private_bug_assignee(self): |
623 | + # Assigning a user to a private bug does not give them visibility. |
624 | owner = self.factory.makePerson() |
625 | assignee = self.factory.makePerson() |
626 | bug = self.factory.makeBug( |
627 | @@ -99,7 +142,22 @@ |
628 | self.assertContentEqual( |
629 | [owner], bug.getBugNotificationRecipients()) |
630 | |
631 | + def test_private_bug_assignee_with_access(self): |
632 | + # An assignee with access will get notified. |
633 | + owner = self.factory.makePerson() |
634 | + assignee = self.factory.makePerson() |
635 | + bug = self.factory.makeBug( |
636 | + owner=owner, information_type=InformationType.USERDATA) |
637 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
638 | + self.factory.makeAccessArtifactGrant( |
639 | + artifact=artifact, grantee=assignee) |
640 | + with person_logged_in(owner): |
641 | + bug.default_bugtask.transitionToAssignee(assignee) |
642 | + self.assertContentEqual( |
643 | + [owner, assignee], bug.getBugNotificationRecipients()) |
644 | + |
645 | def test_private_bug_with_duplicate_subscriber(self): |
646 | + # A subscriber to a duplicate of a private bug will not be notified. |
647 | owner = self.factory.makePerson() |
648 | subscriber = self.factory.makePerson() |
649 | bug = self.factory.makeBug( |
650 | @@ -110,3 +168,20 @@ |
651 | dupe.markAsDuplicate(bug) |
652 | self.assertContentEqual( |
653 | [owner], bug.getBugNotificationRecipients()) |
654 | + |
655 | + def test_private_bug_with_duplicate_subscriber_with_access(self): |
656 | + # A subscriber to a duplicate of a private bug will be notified, if |
657 | + # they have access. |
658 | + owner = self.factory.makePerson() |
659 | + subscriber = self.factory.makePerson() |
660 | + bug = self.factory.makeBug( |
661 | + owner=owner, information_type=InformationType.USERDATA) |
662 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
663 | + self.factory.makeAccessArtifactGrant( |
664 | + artifact=artifact, grantee=subscriber) |
665 | + dupe = self.factory.makeBug(owner=owner) |
666 | + with person_logged_in(owner): |
667 | + dupe.subscribe(subscriber, owner) |
668 | + dupe.markAsDuplicate(bug) |
669 | + self.assertContentEqual( |
670 | + [owner, subscriber], bug.getBugNotificationRecipients()) |
671 | |
672 | === modified file 'lib/lp/bugs/tests/test_bugchanges.py' |
673 | --- lib/lp/bugs/tests/test_bugchanges.py 2012-07-19 04:40:03 +0000 |
674 | +++ lib/lp/bugs/tests/test_bugchanges.py 2012-07-31 01:59:19 +0000 |
675 | @@ -31,7 +31,6 @@ |
676 | from lp.services.webapp.publisher import canonical_url |
677 | from lp.testing import ( |
678 | api_url, |
679 | - celebrity_logged_in, |
680 | launchpadlib_for, |
681 | login_person, |
682 | person_logged_in, |
683 | @@ -1046,83 +1045,6 @@ |
684 | expected_activity=expected_activity, |
685 | expected_notification=expected_notification) |
686 | |
687 | - def _test_retarget_private_security_bug_to_product(self, |
688 | - bug, maintainer, |
689 | - bug_supervisor=None): |
690 | - # When a private security related bug has a bugtask retargetted to a |
691 | - # different product, a notification is sent to the new bug supervisor |
692 | - # and maintainer. If they are the same person, only one notification |
693 | - # is sent. They only get notifications if they can see the bug. |
694 | - |
695 | - # Create the private bug. |
696 | - bug_task = bug.bugtasks[0] |
697 | - new_target = self.factory.makeProduct( |
698 | - owner=maintainer, bug_supervisor=bug_supervisor) |
699 | - self.saveOldChanges(bug) |
700 | - |
701 | - bug_task_before_modification = Snapshot( |
702 | - bug_task, providing=providedBy(bug_task)) |
703 | - bug_task.transitionToTarget(new_target, self.user) |
704 | - notify(ObjectModifiedEvent( |
705 | - bug_task, bug_task_before_modification, |
706 | - ['target', 'product'], user=self.user)) |
707 | - |
708 | - expected_activity = { |
709 | - 'person': self.user, |
710 | - 'whatchanged': 'affects', |
711 | - 'oldvalue': bug_task_before_modification.bugtargetname, |
712 | - 'newvalue': bug_task.bugtargetname, |
713 | - } |
714 | - |
715 | - expected_recipients = [self.user] |
716 | - expected_reasons = ['Subscriber'] |
717 | - if bug.userCanView(maintainer): |
718 | - expected_recipients.append(maintainer) |
719 | - expected_reasons.append('Maintainer') |
720 | - if (bug_supervisor and not bug_supervisor.inTeam(maintainer) |
721 | - and bug.userCanView(bug_supervisor)): |
722 | - expected_recipients.append(bug_supervisor) |
723 | - expected_reasons.append('Bug Supervisor') |
724 | - expected_notification = { |
725 | - 'text': u"** Project changed: %s => %s" % ( |
726 | - bug_task_before_modification.bugtargetname, |
727 | - bug_task.bugtargetname), |
728 | - 'person': self.user, |
729 | - 'recipients': expected_recipients, |
730 | - 'recipient_reasons': expected_reasons |
731 | - } |
732 | - self.assertRecordedChange( |
733 | - expected_activity=expected_activity, |
734 | - expected_notification=expected_notification, bug=bug) |
735 | - |
736 | - def test_retarget_private_security_bug_to_product(self): |
737 | - # A series of tests for re-targetting a private bug task. |
738 | - bug = self.factory.makeBug( |
739 | - product=self.product, owner=self.user, |
740 | - information_type=InformationType.USERDATA) |
741 | - maintainer = self.factory.makePerson() |
742 | - bug_supervisor = self.factory.makePerson() |
743 | - |
744 | - # Test with no bug supervisor |
745 | - self._test_retarget_private_security_bug_to_product(bug, maintainer) |
746 | - # Test with bug supervisor = maintainer. |
747 | - self._test_retarget_private_security_bug_to_product( |
748 | - bug, maintainer, maintainer) |
749 | - # Test with different bug supervisor |
750 | - self._test_retarget_private_security_bug_to_product( |
751 | - bug, maintainer, bug_supervisor) |
752 | - |
753 | - # Now make the bug visible to the bug supervisor and re-test. |
754 | - with celebrity_logged_in('admin'): |
755 | - bug.default_bugtask.transitionToAssignee(bug_supervisor) |
756 | - |
757 | - # Test with bug supervisor = maintainer. |
758 | - self._test_retarget_private_security_bug_to_product( |
759 | - bug, maintainer, maintainer) |
760 | - # Test with different bug supervisor |
761 | - self._test_retarget_private_security_bug_to_product( |
762 | - bug, maintainer, bug_supervisor) |
763 | - |
764 | def test_target_bugtask_to_sourcepackage(self): |
765 | # When a bugtask's target is changed, BugActivity and |
766 | # BugNotification get updated. |
767 | |
768 | === modified file 'lib/lp/registry/model/accesspolicy.py' |
769 | --- lib/lp/registry/model/accesspolicy.py 2012-06-19 02:14:21 +0000 |
770 | +++ lib/lp/registry/model/accesspolicy.py 2012-07-31 01:59:19 +0000 |
771 | @@ -334,7 +334,7 @@ |
772 | |
773 | @classmethod |
774 | def revokeByArtifact(cls, artifacts, grantees=None): |
775 | - """See `IAccessPolicyGrantSource`.""" |
776 | + """See `IAccessArtifactGrantSource`.""" |
777 | cls.findByArtifact(artifacts, grantees).remove() |
778 | |
779 | |
780 | |
781 | === modified file 'lib/lp/registry/tests/test_sharingjob.py' |
782 | --- lib/lp/registry/tests/test_sharingjob.py 2012-07-19 04:40:03 +0000 |
783 | +++ lib/lp/registry/tests/test_sharingjob.py 2012-07-31 01:59:19 +0000 |
784 | @@ -19,7 +19,6 @@ |
785 | from lp.registry.enums import InformationType |
786 | from lp.registry.interfaces.accesspolicy import ( |
787 | IAccessArtifactGrantSource, |
788 | - IAccessArtifactSource, |
789 | IAccessPolicySource, |
790 | ) |
791 | from lp.registry.interfaces.person import TeamSubscriptionPolicy |
792 | @@ -182,9 +181,9 @@ |
793 | job, job_type = create_job(distro, bug, grantee, owner) |
794 | # Subscribing grantee has created an artifact grant so we need to |
795 | # revoke that to test the job. |
796 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
797 | getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
798 | - getUtility(IAccessArtifactSource).find( |
799 | - [bug]), [grantee]) |
800 | + [artifact], [grantee]) |
801 | transaction.commit() |
802 | |
803 | out, err, exit_code = run_script( |
804 | @@ -295,9 +294,9 @@ |
805 | CodeReviewNotificationLevel.NOEMAIL, owner) |
806 | # Subscribing policy_team_grantee has created an artifact grant so we |
807 | # need to revoke that to test the job. |
808 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
809 | getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
810 | - getUtility(IAccessArtifactSource).find( |
811 | - [bug]), [policy_team_grantee]) |
812 | + [artifact], [policy_team_grantee]) |
813 | |
814 | # policy grantees are subscribed because the job has not been run yet. |
815 | subscribers = removeSecurityProxy(bug).getDirectSubscribers() |
816 | @@ -367,9 +366,9 @@ |
817 | bug.subscribe(artifact_indirect_grantee, owner) |
818 | # Subscribing policy_team_grantee has created an artifact grant so we |
819 | # need to revoke that to test the job. |
820 | + artifact = self.factory.makeAccessArtifact(concrete=branch) |
821 | getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
822 | - getUtility(IAccessArtifactSource).find( |
823 | - [branch]), [policy_team_grantee]) |
824 | + [artifact], [policy_team_grantee]) |
825 | |
826 | # policy grantees are subscribed because the job has not been run yet. |
827 | #subscribers = removeSecurityProxy(branch).subscribers |
828 | @@ -432,10 +431,9 @@ |
829 | bug.subscribe(grantee, owner) |
830 | # Subscribing grantee to bug creates an access grant so we need to |
831 | # revoke that for our test. |
832 | - accessartifact_source = getUtility(IAccessArtifactSource) |
833 | - accessartifact_grant_source = getUtility(IAccessArtifactGrantSource) |
834 | - accessartifact_grant_source.revokeByArtifact( |
835 | - accessartifact_source.find([bug]), [grantee]) |
836 | + artifact = self.factory.makeAccessArtifact(concrete=bug) |
837 | + getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
838 | + [artifact], [grantee]) |
839 | |
840 | return bug, owner |
841 |
Looking very good, Steve. I have many comments, as always, including a couple of bugs.
152 -The upstream maintainer will be subscribed to security-related private
153 -bugs, because upstream has no security contact, in this case.
This branch is a prerequisite for the work that will make this test obsolete, but I believe it's still relevant. It's talking about an actual BugSubscription here.
170 === modified file 'lib/lp/ bugs/doc/ security- teams.txt'
Likewise with both changes here. This branch only enables implicit subscriptions -- we still need to test that explicit subscriptions work until we remove them.
201 - def getBugNotificat ionRecipients( duplicateof= None, old_bug=None, master_ dupe_subscriber s=False) : ionRecipients( duplicateof= None, old_bug=None):
202 - include_
203 + def getBugNotificat
Nice cleanup of a dead codepath.
278 + def forbidden_ recipients_ filter( self, column=None):
This actually filters to the allowed recipients, not the forbidden ones. I also wonder if it would be better to return a single expression, which is the Or or True, letting callsites just include it inline without extend()ing. And is it really worth having column= be optional?
337 + all = Select( BugTask. assigneeID, BugTask.bug == self.bug) Person. id.is_in( all))
338 + assignees = load_people(
Why have you split the subquery out into a separate local? Might as well inline it, or at least not shadow a builtin. Also, did you consider inlining the subsequent filter into load_people's where argument?
368 + return subscribers. difference( subscribers_ at_all_ levels,
369 + self.direct_
Direct subscribers will usually have a grant, but not always (eg. after their project access has been revoked, but the sub removal job hasn't run yet). This needs to be filtered too.
392 + if bug.private: bug_privacy_ filter_ terms( ription. subscriberID) ),
393 + filters.extend([
394 + Or(*get_
395 + StructuralSubsc
396 + BugTaskFlat.bug == bug])
I'm a little concerned about this. The function didn't previously return nothing for private bugs, so this may be the wrong level to filter at. Did you consider other locations?
424 - def test_structural _bug_supervisor _becomes_ direct_ on_private( self):
425 - # If a bug supervisor has a structural subscription to the bug, and
426 - # the bug is marked as private, the supervisor should get a direct
427 - # subscription. Otherwise they should be removed, per other tests.
Why is this test no longer valid? Yes, the behaviour should be removed later, but this branch doesn't do it.
448 - def test_informatio n_type_ does_not_ leak(self) :
449 - # Make sure that bug notifications for private bugs do not leak to
450 - # people with a subscription on the product.
This seems like a reasonable integration test to retain. You've added a test that sharing => notification in test_private_ bug_with_ structural_ subscriber_ with_access, but unless I've missed one this is the only one that tests !sharing => !notification.
797 - def test_private_ bug_target_ change_ doesnt_ add_everyone( self):
Again, I don't see that this test's value is gone.