Merge lp:~brian-murray/launchpad/bug-546078 into lp:launchpad

Proposed by Brian Murray
Status: Merged
Approved by: Deryck Hodge
Approved revision: no longer in the source branch.
Merged at revision: 10959
Proposed branch: lp:~brian-murray/launchpad/bug-546078
Merge into: lp:launchpad
Diff against target: 687 lines (+357/-35)
11 files modified
lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py (+3/-3)
lib/lp/bugs/doc/bugtask-search.txt (+170/-3)
lib/lp/bugs/interfaces/bug.py (+1/-1)
lib/lp/bugs/interfaces/bugtarget.py (+3/-1)
lib/lp/bugs/interfaces/bugtask.py (+6/-3)
lib/lp/bugs/model/bugtarget.py (+1/-0)
lib/lp/bugs/model/bugtask.py (+45/-2)
lib/lp/bugs/stories/patches-view/patches-view.txt (+101/-7)
lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt (+1/-1)
lib/lp/bugs/stories/webservice/xx-bug.txt (+6/-5)
lib/lp/registry/tests/test_person.py (+20/-9)
To merge this branch: bzr merge lp:~brian-murray/launchpad/bug-546078
Reviewer Review Type Date Requested Status
Eleanor Berger (community) Approve
Review via email: mp+25740@code.launchpad.net

Description of the change

This branch modifies the +patches view so that bug tasks that a person, or team, are structurally subscribed to appear in the +patches view in addition to those that already appear. This fixes bug 546078.

Additionally, some drive by fixes, for issues I found when reading code, were made:
adding the new expired status to the test_bugtarget_patches_view.py test
wording change of expired definition in lib/lp/bugs/interfaces/bugtask.py
wording change of doc string for get_related_bugtasks_search_params in lib/lp/bugs/model/bugtask.py
typo fix for distribution drivers in lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt

Tests modified:
lib/lp/bugs/doc/bugtask-search.txt
lib/lp/bugs/stories/patches-view/patches-view.txt

To post a comment you must log in.
Revision history for this message
Eleanor Berger (intellectronica) wrote :

Very nice, everything looks pretty good, but there's, I think, one problem. If I understand the code correctly, this works only for subscriptions to products, not to other subscription targets (like packages, project groups, etc). When or before fixing this, it's worth extending the test to cover those other subscription targets too.

review: Needs Information
Revision history for this message
Eleanor Berger (intellectronica) wrote :

As we discussed on IRC, this branch now looks very good. We had one concern, over duplicate results because of the UNION of a subscription to a target and its parent target (like Product and ProjectGroup), so you're going to verify that this isn't the case and fix that if necessary. After that you can land.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py'
2--- lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2010-03-11 22:14:51 +0000
3+++ lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2010-06-07 18:13:25 +0000
4@@ -15,7 +15,6 @@
5 from lp.bugs.interfaces.bugtask import BugTaskStatus
6 from lp.testing import TestCaseWithFactory
7
8-
9 DISPLAY_BUG_STATUS_FOR_PATCHES = {
10 BugTaskStatus.NEW: True,
11 BugTaskStatus.INCOMPLETE: True,
12@@ -27,6 +26,7 @@
13 BugTaskStatus.FIXCOMMITTED: True,
14 BugTaskStatus.FIXRELEASED: False,
15 BugTaskStatus.UNKNOWN: False,
16+ BugTaskStatus.EXPIRED: False
17 }
18
19
20@@ -54,8 +54,8 @@
21
22 def test_status_of_bugs_with_patches_shown(self):
23 # Bugs with patches that have the status FIXRELEASED, INVALID,
24- # WONTFIX, UNKNOWN are not shown in the +patches view; all other
25- # bugs are shown.
26+ # WONTFIX, UNKNOWN, EXPIRED are not shown in the +patches view; all
27+ # other bugs are shown.
28 number_of_bugs_shown = 0
29 for bugtask_status in DISPLAY_BUG_STATUS_FOR_PATCHES:
30 if DISPLAY_BUG_STATUS_FOR_PATCHES[bugtask_status]:
31
32=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
33--- lib/lp/bugs/doc/bugtask-search.txt 2010-04-12 07:53:48 +0000
34+++ lib/lp/bugs/doc/bugtask-search.txt 2010-06-07 18:13:25 +0000
35@@ -92,8 +92,8 @@
36
37 === Product bug supervisor ===
38
39-If No Privileges is specified as Firefox' bug supervisor, searching for his
40-bugs return all of Firefox' bugs.
41+If No Privileges is specified as Firefox's bug supervisor, searching for his
42+bugs return all of Firefox's bugs.
43
44 >>> login('foo.bar@canonical.com')
45 >>> from canonical.launchpad.ftests import syncUpdate
46@@ -989,7 +989,7 @@
47 4 2
48 5 1
49
50-Similary, we can search for bugs that do not have any linked branches.
51+Similarly, we can search for bugs that do not have any linked branches.
52
53 >>> from lp.bugs.interfaces.bugtask import BugBranchSearch
54 >>> search_params = BugTaskSearchParams(
55@@ -1197,3 +1197,170 @@
56 ... importance=[BugTaskImportance.LOW, BugTaskImportance.MEDIUM]))
57 4 Mozilla Firefox Reflow problems with complex page layouts NEW MEDIUM
58 1 Mozilla Firefox Firefox does not support SVG NEW LOW
59+
60+
61+== Searching by structural subscriber ==
62+
63+The 'structural_subscriber' search parameter allows one to search all the bug
64+tasks to which a person is structurally subscribed. A person can be a
65+structural subscriber for a product, a product series, a project, a milestone,
66+a distribution, a distribution series and a distribution source package. No
67+Privileges Person isn't a structural subscriber, so no bug tasks are found:
68+
69+ >>> from canonical.launchpad.interfaces import IPersonSet
70+ >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
71+ >>> no_priv_struct_sub = BugTaskSearchParams(
72+ ... user=None, structural_subscriber=no_priv)
73+ >>> found_bugtasks = bugtask_set.search(no_priv_struct_sub)
74+ >>> found_bugtasks.count()
75+ 0
76+
77+Create a new person and make them a subscriber to all Firefox (product) bug
78+reports. Subsequently, we confirm that they are subscribed to all of the
79+Firefox bug tasks.
80+
81+ >>> product_struct_subber = factory.makePerson(
82+ ... name='product-struct-subber')
83+ >>> firefox.addBugSubscription(product_struct_subber,
84+ ... product_struct_subber)
85+ <StructuralSubscription at ...>
86+ >>> product_struct_sub_search = BugTaskSearchParams(
87+ ... user=None, structural_subscriber=product_struct_subber)
88+ >>> found_bugtasks = bugtask_set.search(product_struct_sub_search)
89+ >>> found_bugtasks.count()
90+ 7
91+
92+Create a new person and subscribe them to all of the bug tasks for a product
93+series. We then test to see that they are subscribed to all of the bug tasks
94+for the product series in which they are interested.
95+
96+ >>> product_series = firefox.getSeries('1.0')
97+ >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False)
98+ >>> series_tasks = product_series.searchTasks(all_targeted)
99+ >>> series_struct_subber = factory.makePerson(
100+ ... name='series-struct-subber')
101+ >>> product_series.addBugSubscription(series_struct_subber,
102+ ... series_struct_subber)
103+ <StructuralSubscription at ...>
104+ >>> series_struct_sub_search = BugTaskSearchParams(
105+ ... user=None, structural_subscriber=series_struct_subber)
106+ >>> found_bugtasks = bugtask_set.search(series_struct_sub_search)
107+ >>> found_bugtasks.count()
108+ 2
109+
110+Create a new product which will be a part of a project group. A bug is
111+created for the product which should then show up for structural subscribers
112+of the project group. Then a new person is created who is subscribed to all
113+of the bug reports about the project. Search for bug tasks that this new
114+person is subscribed.
115+
116+ >>> product = factory.makeProduct()
117+ >>> bug = factory.makeBug(product=product)
118+ >>> project = factory.makeProject()
119+ >>> product.project = project
120+ >>> project_struct_subber = factory.makePerson(
121+ ... name='project-struct-subber')
122+ >>> project.addBugSubscription(project_struct_subber,
123+ ... project_struct_subber)
124+ <StructuralSubscription at ...>
125+ >>> project_struct_sub_search = BugTaskSearchParams(
126+ ... user=None, structural_subscriber=project_struct_subber)
127+ >>> found_bugtasks = bugtask_set.search(project_struct_sub_search)
128+ >>> found_bugtasks.count()
129+ 1
130+
131+We will also subscribe this project subscriber to a product that is a part of
132+the project and ensure that duplicate bug tasks do not appear in the search
133+results.
134+
135+ >>> product2 = factory.makeProduct()
136+ >>> bug = factory.makeBug(product=product2)
137+ >>> product2.project = project
138+ >>> product2.addBugSubscription(project_struct_subber,
139+ ... project_struct_subber)
140+ <StructuralSubscription at ...>
141+ >>> project_struct_sub_search = BugTaskSearchParams(
142+ ... user=None, structural_subscriber=project_struct_subber)
143+ >>> found_bugtasks = bugtask_set.search(project_struct_sub_search)
144+ >>> found_bugtasks.count()
145+ 2
146+
147+Create a new person and subscribe them to all of bug tasks targeted to a
148+milestone. We then test to see that they are subscribed to all of the
149+bug tasks for the milestone in which they are interested.
150+
151+ >>> milestone_struct_subber = factory.makePerson(
152+ ... name='milestone-struct-subber')
153+ >>> product_milestone.addBugSubscription(milestone_struct_subber,
154+ ... milestone_struct_subber)
155+ <StructuralSubscription at ...>
156+ >>> milestone_struct_sub_search = BugTaskSearchParams(
157+ ... user=None, structural_subscriber=milestone_struct_subber)
158+ >>> found_bugtasks = bugtask_set.search(milestone_struct_sub_search)
159+ >>> found_bugtasks.count()
160+ 2
161+
162+Create another new person and subscribe them to all the Ubuntu bug reports -
163+crazy I know. Then test to see that this poor person is subscribed to all
164+bugs with an Ubuntu bug task.
165+
166+ >>> distro_struct_subber = factory.makePerson(
167+ ... name='distro-struct-subber')
168+ >>> ubuntu.addBugSubscription(distro_struct_subber,
169+ ... distro_struct_subber)
170+ <StructuralSubscription at ...>
171+ >>> distro_struct_sub_search = BugTaskSearchParams(
172+ ... user=None, structural_subscriber=distro_struct_subber)
173+ >>> found_bugtasks = bugtask_set.search(distro_struct_sub_search)
174+ >>> found_bugtasks.count()
175+ 5
176+
177+Create a new person who will only be subscribed to an Ubuntu series, which is
178+something more reasonable than all of Ubuntu. Test to ensure that this person
179+is subscribed to all of the bug tasks about that distro series.
180+
181+ >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
182+ >>> hoary = ubuntu.getSeries('hoary')
183+ >>> distro_series_struct_subber = factory.makePerson(
184+ ... name='distro-series-struct-subber')
185+ >>> hoary.addBugSubscription(distro_series_struct_subber,
186+ ... distro_series_struct_subber)
187+ <StructuralSubscription at ...>
188+ >>> distro_series_struct_sub_search = BugTaskSearchParams(
189+ ... user=None, structural_subscriber=distro_series_struct_subber)
190+ >>> found_bugtasks = bugtask_set.search(distro_series_struct_sub_search)
191+ >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False)
192+ >>> hoary_bugtasks = hoary.searchTasks(all_targeted)
193+ >>> found_bugtasks.count() == hoary_bugtasks.count()
194+ True
195+
196+Create a new person and make them a subscriber to all Ubuntu Firefox (a
197+distribution source package) bug reports. Test to see that the new person is
198+subscribed to all of the Ubuntu Firefox bug tasks.
199+
200+ >>> package_struct_subber = factory.makePerson(
201+ ... name='package-struct-subber')
202+ >>> ubuntu_firefox.addBugSubscription(package_struct_subber,
203+ ... package_struct_subber)
204+ <StructuralSubscription at ...>
205+ >>> package_struct_sub_search = BugTaskSearchParams(
206+ ... user=None, structural_subscriber=package_struct_subber)
207+ >>> found_bugtasks = bugtask_set.search(package_struct_sub_search)
208+ >>> found_bugtasks.count()
209+ 1
210+
211+We'll also subscribe the person who is currently subscribed to a package's bug
212+reports, package_struct_subber, to the bug reports of a product series to
213+ensure that the structural_subscriber search is returning the set of both bug
214+tasks.
215+
216+ >>> product_series.addBugSubscription(package_struct_subber,
217+ ... package_struct_subber)
218+ <StructuralSubscription at ...>
219+ >>> package_struct_sub_search = BugTaskSearchParams(
220+ ... user=None, structural_subscriber=package_struct_subber)
221+ >>> found_bugtasks = bugtask_set.search(package_struct_sub_search)
222+ >>> combined_bugtasks_count = (ubuntu_firefox_bugs.count () +
223+ ... series_tasks.count())
224+ >>> found_bugtasks.count() == combined_bugtasks_count
225+ True
226
227=== modified file 'lib/lp/bugs/interfaces/bug.py'
228--- lib/lp/bugs/interfaces/bug.py 2010-06-04 02:29:33 +0000
229+++ lib/lp/bugs/interfaces/bug.py 2010-06-07 18:13:25 +0000
230@@ -97,7 +97,7 @@
231 False in a boolean context, or an AssertionError will be raised.
232
233 If distribution is specified, sourcepackagename may optionally
234- be provided. product must evaluate to False in a boolean
235+ be provided. Product must evaluate to False in a boolean
236 context, or an AssertionError will be raised.
237 """
238 assert product or distribution, (
239
240=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
241--- lib/lp/bugs/interfaces/bugtarget.py 2010-05-06 01:46:55 +0000
242+++ lib/lp/bugs/interfaces/bugtarget.py 2010-06-07 18:13:25 +0000
243@@ -76,6 +76,7 @@
244 bug_supervisor=Reference(schema=Interface),
245 bug_commenter=Reference(schema=Interface),
246 bug_subscriber=Reference(schema=Interface),
247+ structural_subscriber=Reference(schema=Interface),
248 owner=Reference(schema=Interface),
249 affected_user=Reference(schema=Interface),
250 has_patch=copy_field(IBugTaskSearch['has_patch']),
251@@ -184,7 +185,8 @@
252 hardware_owner_is_bug_reporter=None,
253 hardware_owner_is_affected_by_bug=False,
254 hardware_owner_is_subscribed_to_bug=False,
255- hardware_is_linked_to_bug=False, linked_branches=None):
256+ hardware_is_linked_to_bug=False, linked_branches=None,
257+ structural_subscriber=None):
258 """Search the IBugTasks reported on this entity.
259
260 :search_params: a BugTaskSearchParams object
261
262=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
263--- lib/lp/bugs/interfaces/bugtask.py 2010-05-25 16:45:26 +0000
264+++ lib/lp/bugs/interfaces/bugtask.py 2010-06-07 18:13:25 +0000
265@@ -175,7 +175,7 @@
266 EXPIRED = DBItem(19, """
267 Expired
268
269- This bug is expired. There was no activity since a longer time.
270+ This bug is expired. There was no activity for a long time.
271 """)
272
273 CONFIRMED = DBItem(20, """
274@@ -1075,7 +1075,7 @@
275 hardware_owner_is_affected_by_bug=False,
276 hardware_owner_is_subscribed_to_bug=False,
277 hardware_is_linked_to_bug=False,
278- linked_branches=None
279+ linked_branches=None, structural_subscriber=None
280 ):
281
282 self.bug = bug
283@@ -1120,6 +1120,7 @@
284 hardware_owner_is_subscribed_to_bug)
285 self.hardware_is_linked_to_bug = hardware_is_linked_to_bug
286 self.linked_branches = linked_branches
287+ self.structural_subscriber = structural_subscriber
288
289 def setProduct(self, product):
290 """Set the upstream context on which to filter the search."""
291@@ -1192,7 +1193,8 @@
292 hardware_owner_is_bug_reporter=None,
293 hardware_owner_is_affected_by_bug=False,
294 hardware_owner_is_subscribed_to_bug=False,
295- hardware_is_linked_to_bug=False, linked_branches=None):
296+ hardware_is_linked_to_bug=False, linked_branches=None,
297+ structural_subscriber=None):
298 """Create and return a new instance using the parameter list."""
299 search_params = cls(user=user, orderby=order_by)
300
301@@ -1260,6 +1262,7 @@
302 search_params.hardware_is_linked_to_bug = (
303 hardware_is_linked_to_bug)
304 search_params.linked_branches=linked_branches
305+ search_params.structural_subscriber = structural_subscriber
306
307 return search_params
308
309
310=== modified file 'lib/lp/bugs/model/bugtarget.py'
311--- lib/lp/bugs/model/bugtarget.py 2010-04-19 08:31:01 +0000
312+++ lib/lp/bugs/model/bugtarget.py 2010-06-07 18:13:25 +0000
313@@ -51,6 +51,7 @@
314 importance=None,
315 assignee=None, bug_reporter=None, bug_supervisor=None,
316 bug_commenter=None, bug_subscriber=None, owner=None,
317+ structural_subscriber=None,
318 affected_user=None, affects_me=False,
319 has_patch=None, has_cve=None, distribution=None,
320 tags=None, tags_combinator=BugTagsSearchCombinator.ALL,
321
322=== modified file 'lib/lp/bugs/model/bugtask.py'
323--- lib/lp/bugs/model/bugtask.py 2010-05-27 13:51:06 +0000
324+++ lib/lp/bugs/model/bugtask.py 2010-06-07 18:13:25 +0000
325@@ -149,7 +149,7 @@
326 search for all tasks related to a user given by `context`.
327
328 Which tasks are related to a user?
329- * the user has to be either assignee or owner of this tasks
330+ * the user has to be either assignee or owner of this task
331 OR
332 * the user has to be subscriber or commenter to the underlying bug
333 OR
334@@ -158,7 +158,8 @@
335 always get one task owned by the bug reporter
336 """
337 assert IPerson.providedBy(context), "Context argument needs to be IPerson"
338- relevant_fields = ('assignee', 'bug_subscriber', 'owner', 'bug_commenter')
339+ relevant_fields = ('assignee', 'bug_subscriber', 'owner', 'bug_commenter',
340+ 'structural_subscriber')
341 search_params = []
342 for key in relevant_fields:
343 # all these parameter default to None
344@@ -1641,6 +1642,48 @@
345 BugSubscription.person = %(personid)s""" %
346 sqlvalues(personid=params.subscriber.id))
347
348+ if params.structural_subscriber is not None:
349+ structural_subscriber_clause = ( """BugTask.id IN (
350+ SELECT BugTask.id FROM BugTask, StructuralSubscription
351+ WHERE BugTask.product = StructuralSubscription.product
352+ AND StructuralSubscription.subscriber = %(personid)s
353+ UNION ALL
354+ SELECT BugTask.id FROM BugTask, StructuralSubscription
355+ WHERE
356+ BugTask.distribution = StructuralSubscription.distribution
357+ AND BugTask.sourcepackagename =
358+ StructuralSubscription.sourcepackagename
359+ AND StructuralSubscription.subscriber = %(personid)s
360+ UNION ALL
361+ SELECT BugTask.id FROM BugTask, StructuralSubscription
362+ WHERE
363+ BugTask.distroseries = StructuralSubscription.distroseries
364+ AND StructuralSubscription.subscriber = %(personid)s
365+ UNION ALL
366+ SELECT BugTask.id FROM BugTask, StructuralSubscription
367+ WHERE
368+ BugTask.milestone = StructuralSubscription.milestone
369+ AND StructuralSubscription.subscriber = %(personid)s
370+ UNION ALL
371+ SELECT BugTask.id FROM BugTask, StructuralSubscription
372+ WHERE
373+ BugTask.productseries = StructuralSubscription.productseries
374+ AND StructuralSubscription.subscriber = %(personid)s
375+ UNION ALL
376+ SELECT BugTask.id FROM BugTask, StructuralSubscription, Product
377+ WHERE
378+ BugTask.product = Product.id
379+ AND Product.project = StructuralSubscription.project
380+ AND StructuralSubscription.subscriber = %(personid)s
381+ UNION ALL
382+ SELECT BugTask.id FROM BugTask, StructuralSubscription
383+ WHERE
384+ BugTask.distribution = StructuralSubscription.distribution
385+ AND StructuralSubscription.sourcepackagename is NULL
386+ AND StructuralSubscription.subscriber = %(personid)s)""" %
387+ sqlvalues(personid=params.structural_subscriber))
388+ extra_clauses.append(structural_subscriber_clause)
389+
390 if params.component:
391 clauseTables += ["SourcePackagePublishingHistory",
392 "SourcePackageRelease"]
393
394=== modified file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
395--- lib/lp/bugs/stories/patches-view/patches-view.txt 2010-04-16 13:01:51 +0000
396+++ lib/lp/bugs/stories/patches-view/patches-view.txt 2010-06-07 18:13:25 +0000
397@@ -431,6 +431,100 @@
398 Tabs: ...
399 Main heading: Patch attachments for Patchy Person
400
401+The patches view for a person or team will also show patches to which a person
402+or team is structurally subscribed. Structural subscription searching is
403+thoroughly tested in bugtask-search.txt so here a single structural
404+subscription will be tested.
405+
406+ >>> from canonical.launchpad.ftests import login, logout
407+ >>> project_subscriber = factory.doAsUser(
408+ ... 'foo.bar@canonical.com', factory.makePerson,
409+ ... name="project-subscriber", displayname="Project Subscriber")
410+ >>> login('foo.bar@canonical.com')
411+ >>> patchy_product.addBugSubscription(project_subscriber,
412+ ... project_subscriber)
413+ <StructuralSubscription at ...>
414+ >>> logout()
415+ >>> from zope.security.proxy import removeSecurityProxy
416+ >>> subscriber_name = removeSecurityProxy(project_subscriber).name
417+ >>> anon_browser.open(
418+ ... 'http://bugs.launchpad.dev/~%s/+patches' % subscriber_name)
419+ >>> show_patches_view(anon_browser.contents)
420+ Bug Importance Status Project Patch Age
421+ Bug #18: bug_c title Wishlist Fix Committed patchy-product-1 ...second...
422+ From: Patchy Person
423+ Link: patch_f.diff
424+ description of patch f
425+ Bug #17: bug_b title Critical Confirmed patchy-product-1 ...second...
426+ From: Patchy Person
427+ Link: patch_c.diff
428+ description of patch c
429+ Bug #16: bug_a title Undecided New patchy-product-1 ...second...
430+ From: Patchy Person
431+ Link: patch_a.diff
432+ description of patch a
433+
434+To ensure that all bugs with patches are shown for every structural
435+subscription a new bug and is made to affect the evolution source package for
436+Ubuntu. Additionally, a patch is added to that bug report so that it will
437+show up in the +patches view. Project-subscriber is then subscribed to the
438+evolution source package for Ubuntu and it is tested that both groups of
439+patches are shown.
440+
441+ >>> hacky_product = factory.doAsUser(
442+ ... 'foo.bar@canonical.com', factory.makeProduct,
443+ ... name='hacky-product', displayname="Hacky Product")
444+ >>> transaction.commit()
445+ >>> bug_e = factory.doAsUser(
446+ ... 'foo.bar@canonical.com', make_bug,
447+ ... title="bug_e is for evolution", product=hacky_product,
448+ ... importance=BugTaskImportance.WISHLIST,
449+ ... status=BugTaskStatus.NEW)
450+ >>> factory.doAsUser(
451+ ... 'foo.bar@canonical.com', factory.makeBugAttachment,
452+ ... comment="comment about patch h",
453+ ... filename="patch_h.diff", owner=patch_submitter,
454+ ... description="patch h is for helping", bug=bug_e, is_patch=True)
455+ <BugAttachment at...
456+ >>> factory.doAsUser(
457+ ... 'foo.bar@canonical.com', make_bugtask, bug=bug_e,
458+ ... target='evolution', target_is_spkg_name=True,
459+ ... importance=BugTaskImportance.MEDIUM,
460+ ... status=BugTaskStatus.TRIAGED)
461+ >>> transaction.commit()
462+ >>> from canonical.launchpad.ftests import login, logout
463+ >>> from zope.component import getUtility
464+ >>> from lp.registry.interfaces.distribution import IDistributionSet
465+ >>> login('foo.bar@canonical.com')
466+ >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
467+ >>> ubuntu_evolution = ubuntu.getSourcePackage("evolution")
468+ >>> ubuntu_evolution.addBugSubscription(project_subscriber,
469+ ... project_subscriber)
470+ <StructuralSubscription at ...>
471+ >>> logout()
472+ >>> from zope.security.proxy import removeSecurityProxy
473+ >>> subscriber_name = removeSecurityProxy(project_subscriber).name
474+ >>> anon_browser.open(
475+ ... 'http://bugs.launchpad.dev/~%s/+patches' % subscriber_name)
476+ >>> show_patches_view(anon_browser.contents)
477+ Bug Importance Status Project Patch Age
478+ Bug #20: bug_e is... Medium Triaged evolution ...second...
479+ From: Patchy Person
480+ Link: patch_h.diff
481+ patch h is for helping
482+ Bug #18: bug_c title Wishlist Fix Committed patchy-product-1 ...second...
483+ From: Patchy Person
484+ Link: patch_f.diff
485+ description of patch f
486+ Bug #17: bug_b title Critical Confirmed patchy-product-1 ...second...
487+ From: Patchy Person
488+ Link: patch_c.diff
489+ description of patch c
490+ Bug #16: bug_a title Undecided New patchy-product-1 ...second...
491+ From: Patchy Person
492+ Link: patch_a.diff
493+ description of patch a
494+
495 Reaching the Patches View
496 -------------------------
497
498@@ -496,24 +590,28 @@
499
500 >>> print_bugfilters_portlet_filled(anon_browser, 'ubuntu')
501 6 New bugs
502- 9 Open bugs
503+ 10 Open bugs
504 0 In-progress bugs
505 0 Critical bugs
506 1 High importance bug
507 0 Incomplete bugs (can expire)
508 <BLANKLINE>
509- 5 Bugs with patches
510+ 6 Bugs with patches
511 2 Bugs fixed elsewhere
512 2 Open CVE bugs - CVE reports
513
514 The number of bugs with patches shown in the bugfilter stats portlet
515 might be lower than the number of bugs (bugtasks) listed on the
516 corresponding patches view page, because the latter shows resolved
517-bugs too.
518+bugs too. N.B. only 5 bug tasks will appear at a time.
519
520 >>> anon_browser.open('http://bugs.launchpad.dev/ubuntu/+patches')
521 >>> show_patches_view(anon_browser.contents)
522 Bug Importance Status Package Patch Age
523+ Bug #20: bug_e... Medium Triaged evolution ...second...
524+ From: Patchy Person
525+ Link: patch_h.diff
526+ patch h is for helping
527 Bug #18: bug_c title High Triaged a52dec ...second...
528 From: Patchy Person
529 Link: patch_f.diff
530@@ -530,7 +628,3 @@
531 From: Patchy Person
532 Link: patch_a.diff
533 description of patch a
534- Bug #16: bug_a title Undecided New ubuntu ...second...
535- From: Patchy Person
536- Link: patch_a.diff
537- description of patch a
538
539=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt'
540--- lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt 2009-10-28 18:00:36 +0000
541+++ lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt 2010-06-07 18:13:25 +0000
542@@ -64,7 +64,7 @@
543 Landscape Developers
544
545
546-== Additional options for distribuion drivers ==
547+== Additional options for distribution drivers ==
548
549 When editing the subscriptions for a package, a distribution driver
550 can subscribe any Launchpad user.
551
552=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
553--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-05-18 12:38:19 +0000
554+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-07 18:13:25 +0000
555@@ -1660,7 +1660,7 @@
556 owner_link: u'http://api.launchpad.dev/beta/~testuser1'
557 ...
558
559-`testuser2` is subscribed to `testbugs2`, so this bug is related to this
560+`testuser2` is subscribed to `testbug2`, so this bug is related to this
561 user:
562
563 >>> related = webservice.named_get(
564@@ -1682,15 +1682,16 @@
565 total_size: 0
566 ---
567
568-You are not allowed to overwrite all user related parameter in the same
569-query, because in this case this bug will no be related to the person
570-anymore. In this case a `400 Bad Request`-Error will be returned
571+You are not allowed to overwrite all user related parameters in the same
572+query, because this bug will not be related to the person anymore. In this
573+case a `400 Bad Request`-Error will be returned.
574
575 >>> name12 = webservice.get("/~name12").jsonBody()
576 >>> print webservice.named_get(
577 ... '/~name16', 'searchTasks', assignee=name12['self_link'],
578 ... owner=name12['self_link'], bug_subscriber=name12['self_link'],
579- ... bug_commenter=name12['self_link']
580+ ... bug_commenter=name12['self_link'],
581+ ... structural_subscriber=name12['self_link']
582 ... )
583 HTTP/1.1 400 Bad Request...
584
585
586=== modified file 'lib/lp/registry/tests/test_person.py'
587--- lib/lp/registry/tests/test_person.py 2010-05-07 19:07:28 +0000
588+++ lib/lp/registry/tests/test_person.py 2010-06-07 18:13:25 +0000
589@@ -526,7 +526,8 @@
590
591 def checkUserFields(
592 self, params, assignee=None, bug_subscriber=None,
593- owner=None, bug_commenter=None, bug_reporter=None):
594+ owner=None, bug_commenter=None, bug_reporter=None,
595+ structural_subscriber=None):
596 self.failUnlessEqual(assignee, params.assignee)
597 # fromSearchForm() takes a bug_subscriber parameter, but saves
598 # it as subscriber on the parameter object.
599@@ -534,14 +535,15 @@
600 self.failUnlessEqual(owner, params.owner)
601 self.failUnlessEqual(bug_commenter, params.bug_commenter)
602 self.failUnlessEqual(bug_reporter, params.bug_reporter)
603+ self.failUnlessEqual(structural_subscriber, params.structural_subscriber)
604
605 def test_get_related_bugtasks_search_params(self):
606 # With no specified options, get_related_bugtasks_search_params()
607- # returns 4 BugTaskSearchParams objects, each with a different
608+ # returns 5 BugTaskSearchParams objects, each with a different
609 # user field set.
610 search_params = get_related_bugtasks_search_params(
611 self.user, self.context)
612- self.assertEqual(len(search_params), 4)
613+ self.assertEqual(len(search_params), 5)
614 self.checkUserFields(
615 search_params[0], assignee=self.context)
616 self.checkUserFields(
617@@ -550,13 +552,15 @@
618 search_params[2], owner=self.context, bug_reporter=self.context)
619 self.checkUserFields(
620 search_params[3], bug_commenter=self.context)
621+ self.checkUserFields(
622+ search_params[4], structural_subscriber=self.context)
623
624 def test_get_related_bugtasks_search_params_with_assignee(self):
625 # With assignee specified, get_related_bugtasks_search_params()
626- # returns 3 BugTaskSearchParams objects.
627+ # returns 4 BugTaskSearchParams objects.
628 search_params = get_related_bugtasks_search_params(
629 self.user, self.context, assignee=self.user)
630- self.assertEqual(len(search_params), 3)
631+ self.assertEqual(len(search_params), 4)
632 self.checkUserFields(
633 search_params[0], assignee=self.user, bug_subscriber=self.context)
634 self.checkUserFields(
635@@ -564,19 +568,23 @@
636 bug_reporter=self.context)
637 self.checkUserFields(
638 search_params[2], assignee=self.user, bug_commenter=self.context)
639+ self.checkUserFields(
640+ search_params[3], assignee=self.user, structural_subscriber=self.context)
641
642 def test_get_related_bugtasks_search_params_with_owner(self):
643 # With owner specified, get_related_bugtasks_search_params() returns
644- # 3 BugTaskSearchParams objects.
645+ # 4 BugTaskSearchParams objects.
646 search_params = get_related_bugtasks_search_params(
647 self.user, self.context, owner=self.user)
648- self.assertEqual(len(search_params), 3)
649+ self.assertEqual(len(search_params), 4)
650 self.checkUserFields(
651 search_params[0], owner=self.user, assignee=self.context)
652 self.checkUserFields(
653 search_params[1], owner=self.user, bug_subscriber=self.context)
654 self.checkUserFields(
655 search_params[2], owner=self.user, bug_commenter=self.context)
656+ self.checkUserFields(
657+ search_params[3], owner=self.user, structural_subscriber=self.context)
658
659 def test_get_related_bugtasks_search_params_with_bug_reporter(self):
660 # With bug reporter specified, get_related_bugtasks_search_params()
661@@ -584,7 +592,7 @@
662 # is overwritten in one instance.
663 search_params = get_related_bugtasks_search_params(
664 self.user, self.context, bug_reporter=self.user)
665- self.assertEqual(len(search_params), 4)
666+ self.assertEqual(len(search_params), 5)
667 self.checkUserFields(
668 search_params[0], bug_reporter=self.user,
669 assignee=self.context)
670@@ -599,13 +607,16 @@
671 self.checkUserFields(
672 search_params[3], bug_reporter=self.user,
673 bug_commenter=self.context)
674+ self.checkUserFields(
675+ search_params[4], bug_reporter=self.user,
676+ structural_subscriber=self.context)
677
678 def test_get_related_bugtasks_search_params_illegal(self):
679 self.assertRaises(
680 IllegalRelatedBugTasksParams,
681 get_related_bugtasks_search_params, self.user, self.context,
682 assignee=self.user, owner=self.user, bug_commenter=self.user,
683- bug_subscriber=self.user)
684+ bug_subscriber=self.user, structural_subscriber=self.user)
685
686 def test_get_related_bugtasks_search_params_illegal_context(self):
687 # in case the `context` argument is not of type IPerson an