Merge lp:~sinzui/launchpad/bug-subselect-timeout into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16058
Proposed branch: lp:~sinzui/launchpad/bug-subselect-timeout
Merge into: lp:launchpad
Diff against target: 1373 lines (+119/-1038)
3 files modified
lib/lp/bugs/doc/bugtask-search.txt (+34/-953)
lib/lp/bugs/model/bugtasksearch.py (+62/-85)
lib/lp/bugs/model/tests/test_bugtasksearch.py (+23/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/bug-subselect-timeout
Reviewer Review Type Date Requested Status
William Grant code Approve
j.c.sackett (community) Approve
Review via email: mp+126566@code.launchpad.net

Commit message

Only match the context when searching for structures a person is subscribed to.

Description of the change

The bug query clause for structural subscribers times out. This is
possibly caused by the superfluous joins against bugtaskflat.

--------------------------------------------------------------------

RULES

    Pre-implementation: wgrant
    * Structure the query to be fast. Try In() and Row() to match a CTE table
      of just the relevant structural subscriptions.
    * Do not match impossible conditions...when looking for distro or
      distro packages, do not match on product or product series.

QA

    * Visit https://bugs.qastaging.launchpad.net/ubuntu/+bugs?field.structural_subscriber=foundations-bugs
    * Verify it does not timeout

LINT

    lib/lp/bugs/model/bugtasksearch.py

TEST

    ./bin/test -vvc -t structural lp.bugs.model.tests.test_bugtasksearch

IMPLEMENTATION

Replaced the LEFT JOINS with subselects using In() and Row() as needed. I
tried a single Row() that matched the SS columns to BugTaskFlat columns,
but the tests failed -- I suspect that NULLs in Row() did not match the NULLs
in the SELECT. NILL != NULL. Maybe I botched this. Note the odd rule that
matches DSP SSs to DS SP bugs. This looks historical, but may also be
convenience because SPs can be short-lived and probably causes users to
not ask why they stopped getting email when a new series started. After I
got the tests to pass, I changed the code to build only the subqueries that
make sense for the bug params being used.
    lib/lp/bugs/model/bugtasksearch.py

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks alright to me, but I would wait for William's input as the peculiarities of bug search performance is definitely not something I'm up on.

review: Approve
Revision history for this message
William Grant (wgrant) wrote :

There's a general issue here that your preconditions for applying the restrictions are a little too strict. I'd suggest perhaps maintaining a map of {target type: [potentially relevant subscription targets]}, or just examining all subscription targets unconditionally if that performs reasonably.

I tried doing this in a single ROW ... IN clause as I suggested on the call, but didn't get very far before it tried to strangle me, mostly due to the fact that (distro, spn) tasks must be found by any (distro, NULL) subscription. It'd be possible with a bit of UNIONing, but I'm not sure it's worth it at this stage.

Specific comments inline below.

105 + Select(
106 + SS.distributionID,
107 + tables=[SS],
108 + where=(SS.sourcepackagenameID == None))))

Given how long the structsub code is, it might be worth dropping the newline in the middle there, and similarly compressing the other blocks. Even cutting just a few lines makes the whole thing seem a lot more manageable.

109 + ss_clauses.append(In(
110 + Row(BugTaskFlat.distribution_id,
111 + BugTaskFlat.sourcepackagename_id),
112 + Select(
113 + ((SS.distributionID,
114 + SS.sourcepackagenameID),),
115 + tables=[SS])))

I don't think you need the double bracketing in the Select. Removing that also lets you get it onto one line. Same in a couple of other places.

125 + if params.distroseries is not None:
126 + distroseries_id = params.distroseries.id
127 + parent_distro_id = params.distroseries.distributionID
128 + else:
129 + distroseries_id = 0
130 + parent_distro_id = 0

I've never understood this bit. It looks like it will just ignore DSP structsubs for SP tasks unless queried in a distroseries context?

141 + if is_person_search or params.product is not None:
142 + ss_clauses.append(In(
143 + BugTaskFlat.product_id,
144 + Select(
145 + SS.productID,
146 + tables=[SS])))

What about project groups? They show multiple products, but this will ignore all of the product subscriptions. Also, the entire Select() expression can be one line.

153 + if is_person_search or params.project is not None:

This will break in one obscure case: if searching in a product, it won't return any bugs if the only structural subscription is through the project group.

163 + if is_person_search or params.milestone is not None:

This needs to apply in basically all cases. Milestone subscriptions affect all targets.

183 + # Remove bugtasks from deactivated products.
184 + # This is needed for searches where people are the context.
185 + if is_person_search:

This also breaks in the case of project group searches.

review: Needs Fixing (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for the review William.

I simplified the rules for appending ss clauses. Milestones are always present as you suggested. The two if-statements
check if the query is constrained to product or distro related bugs. This allowed me to remove the is_person_search variable that you pointed out was also applying to project groups. I reduced the number of vertical lines. I also revised the comment about why we search for DSP's when we know the user has constrained the search to a distroseries.

Revision history for this message
William Grant (wgrant) wrote :

That turned out much nicer than I thought it could, thanks! One last nitpick: "if params.product is None and params.productseries is None" can probably be further restricted with "params.project is None", although it's correct either way.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
2--- lib/lp/bugs/doc/bugtask-search.txt 2012-08-16 05:18:54 +0000
3+++ lib/lp/bugs/doc/bugtask-search.txt 2012-09-28 22:06:42 +0000
4@@ -18,114 +18,6 @@
5 True
6
7
8-== Searching by bug supervisor ==
9-
10-The 'bug_supervisor' parameter allows you to search bugtasks that a certain
11-person is responsible for. A person can be a bug supervisor for a product,
12-a distribution, or a distribution source package. No Privileges Person
13-isn't a bug supervisor, so no bugs are found for him:
14-
15- >>> from lp.registry.interfaces.person import IPersonSet
16- >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
17- >>> no_priv_bug_supervisor = BugTaskSearchParams(
18- ... user=None, bug_supervisor=no_priv)
19- >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor)
20- >>> found_bugtasks.count()
21- 0
22-
23-== Product bugs ==
24-
25-Firefox has a few bugs:
26-
27- >>> from lp.registry.interfaces.product import IProductSet
28- >>> firefox = getUtility(IProductSet).getByName('firefox')
29- >>> firefox_bugs = firefox.searchTasks(all_public)
30- >>> firefox_public_bugs = firefox_bugs.count()
31- >>> firefox_public_bugs > 0
32- True
33-
34-== Distribution and package bugs ==
35-
36-Ubuntu does too:
37-
38- >>> from lp.registry.interfaces.distribution import IDistributionSet
39- >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
40- >>> all_public = BugTaskSearchParams(user=None)
41- >>> ubuntu_bugs = ubuntu.searchTasks(all_public)
42- >>> ubuntu_bugs.count() > 0
43- True
44-
45-and in particular, mozilla-firefox in Ubuntu has 'em:
46-
47- >>> ubuntu_firefox = ubuntu.getSourcePackage("mozilla-firefox")
48- >>> ubuntu_firefox_bugs = ubuntu_firefox.searchTasks(all_public)
49- >>> ubuntu_firefox_bugs.count() > 0
50- True
51-
52-== Person bugs ==
53-
54-To get all related tasks to a person call searchTasks() on the person
55-object:
56-
57- >>> from lp.registry.interfaces.person import IPersonSet
58- >>> user = getUtility(IPersonSet).getByName('name16')
59- >>> user_bugs = user.searchTasks(None, user=None)
60- >>> user_bugs.count() > 0
61- True
62-
63-== Dupes and Conjoined tasks ==
64-
65-You can set flags to omit duplicates:
66-
67- >>> no_dupes = BugTaskSearchParams(user=None, omit_dupes=True)
68- >>> firefox = getUtility(IProductSet).getByName('firefox')
69- >>> sans_dupes = firefox.searchTasks(no_dupes)
70- >>> sans_dupes.count() < firefox_public_bugs
71- True
72-
73-and also series-targeted bugs:
74-
75- >>> no_targeted = BugTaskSearchParams(user=None, omit_targeted=True)
76- >>> sans_targeted = ubuntu.searchTasks(no_targeted)
77- >>> sans_targeted.count() < ubuntu_bugs
78- True
79-
80-=== Product bug supervisor ===
81-
82-If No Privileges is specified as Firefox's bug supervisor, searching for his
83-bugs return all of Firefox's bugs.
84-
85- >>> login('foo.bar@canonical.com')
86- >>> firefox.bug_supervisor = no_priv
87-
88- >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor)
89- >>> found_bugtasks.count() == firefox_bugs.count()
90- True
91-
92- >>> found_targets = set(
93- ... bugtask.target.bugtargetdisplayname for bugtask in found_bugtasks)
94- >>> for target_name in sorted(found_targets):
95- ... print target_name
96- Mozilla Firefox
97-
98-
99-=== Distribution bug supervisor ===
100-
101-If someone is bug supervisor for Firefox, Firefox in Ubuntu, and Ubuntu,
102-all bugs in Firefox and Ubuntu are returned. Bugs in the Ubuntu Firefox
103-package are included in the Ubuntu bugs, so they won't be returned
104-twice.
105-
106- >>> all_public = BugTaskSearchParams(user=None)
107- >>> ubuntu_bugs = ubuntu.searchTasks(all_public)
108- >>> ubuntu_bugs.count() > 0
109- True
110-
111- >>> ubuntu.bug_supervisor = no_priv
112- >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor)
113- >>> found_bugtasks.count() == firefox_bugs.count() + ubuntu_bugs.count()
114- True
115-
116 == Searching using bug full-text index ==
117
118 The searchtext parameter does an extensive and expensive search (it
119@@ -136,6 +28,8 @@
120
121 For example, there are no bugs with the word 'Fnord' in Firefox.
122
123+ >>> from lp.registry.interfaces.product import IProductSet
124+ >>> firefox = getUtility(IProductSet).getByName('firefox')
125 >>> text_search = BugTaskSearchParams(user=None, searchtext=u'Fnord')
126 >>> found_bugtasks = firefox.searchTasks(text_search)
127 >>> found_bugtasks.count()
128@@ -144,6 +38,7 @@
129 But if we put that word in the bug #4 description, it will be found.
130
131 >>> from lp.bugs.interfaces.bug import IBugSet
132+ >>> login('foo.bar@canonical.com')
133 >>> bug_four = getUtility(IBugSet).get(4)
134 >>> bug_four.description += (
135 ... '\nThat happens pretty often with the Fnord Highlighter '
136@@ -212,322 +107,6 @@
137 >>> transaction.abort()
138
139
140-== Searching by bug reporter ==
141-
142-The 'bug_reporter' parameter allows you to search for bugs reported by a
143-certain person.
144-
145- >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
146- >>> reported_by_foo_bar = BugTaskSearchParams(
147- ... user=None, bug_reporter=foo_bar)
148- >>> reported_by_foo_bar.setDistribution(ubuntu)
149- >>> found_bugtasks = bugtask_set.search(reported_by_foo_bar)
150- >>> for bugtask in found_bugtasks:
151- ... print "#%s in %s reported by %s" % (
152- ... bugtask.bug.id, bugtask.bugtargetname,
153- ... bugtask.bug.owner.displayname)
154- #9 in thunderbird (Ubuntu) reported by Foo Bar
155- #10 in linux-source-2.6.15 (Ubuntu) reported by Foo Bar
156-
157-
158-== Searching for nominated bugs ==
159-
160-We can search for bugs nominated to a distribution series by using the
161-nominated_for parameter.
162-
163- >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
164- >>> warty = ubuntu.getSeries('warty')
165-
166- >>> from lp.bugs.model.bugnomination import BugNomination
167- >>> print list(BugNomination.selectBy(distroseries=warty))
168- []
169-
170- >>> from lp.bugs.interfaces.bug import CreateBugParams
171- >>> nominated_for_warty = BugTaskSearchParams(
172- ... user=None, nominated_for=warty)
173- >>> list(ubuntu.searchTasks(nominated_for_warty))
174- []
175-
176- >>> nominated_bug = ubuntu.createBug(
177- ... CreateBugParams(owner=no_priv, title='Test nominated bug',
178- ... comment='Something'))
179- >>> BugNomination(
180- ... owner=no_priv, distroseries=warty, bug=nominated_bug)
181- <BugNomination at ...>
182-
183- >>> for bugtask in ubuntu.searchTasks(nominated_for_warty):
184- ... print bugtask.bug.title
185- Test nominated bug
186-
187-The same parameter is used to search for bugs nominated to a product
188-series.
189-
190- >>> firefox = getUtility(IProductSet).getByName('firefox')
191- >>> firefox_trunk = firefox.getSeries('trunk')
192- >>> print list(BugNomination.selectBy(productseries=firefox_trunk))
193- []
194- >>> nominated_for_trunk = BugTaskSearchParams(
195- ... user=None, nominated_for=firefox_trunk)
196- >>> list(firefox.searchTasks(nominated_for_trunk))
197- []
198-
199- >>> nominated_bug = firefox.createBug(
200- ... CreateBugParams(owner=no_priv, title='Bug to be fixed in trunk',
201- ... comment='Something'))
202- >>> BugNomination(
203- ... owner=no_priv, productseries=firefox_trunk, bug=nominated_bug)
204- <BugNomination at ...>
205-
206- >>> for bugtask in firefox.searchTasks(nominated_for_trunk):
207- ... print bugtask.bug.title
208- Bug to be fixed in trunk
209-
210-== Filter by Upstream Status ==
211-
212-Add an Ubuntu bugtask for a bug that is confirmed upstream.
213-
214- >>> from lp.bugs.interfaces.bugtask import (
215- ... BugTaskImportance,
216- ... BugTaskStatus,
217- ... )
218- >>> from lp.bugs.model.tests.test_bugtask import (
219- ... BugTaskSearchBugsElsewhereTest)
220- >>> def bugTaskInfo(bugtask):
221- ... return '%i %i %s %s' % (
222- ... bugtask.id, bugtask.bug.id, bugtask.bugtargetdisplayname,
223- ... bugtask.bug.title)
224- >>> test_helper = BugTaskSearchBugsElsewhereTest(helper_only=True)
225- >>> bug_twelve = getUtility(IBugSet).get(12)
226- >>> task_open_upstream = bugtask_set.createTask(
227- ... bug_twelve, foo_bar, ubuntu,
228- ... status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
229- >>> test_helper.assertBugTaskIsOpenUpstream(task_open_upstream)
230-
231-Pass the resolved_upstream flag to include only bugtasks linked to
232-watches that are rejected, fixed committed or fix released, or bugtasks
233-related to upstream bugtasks (i.e. filed on the same bug) that are fix
234-committed or fix released.
235-
236- >>> test_helper.setUpBugsResolvedUpstreamTests()
237- >>> params = BugTaskSearchParams(
238- ... resolved_upstream=True, orderby='id', user=None)
239- >>> closed_elsewhere_tasks = ubuntu.searchTasks(params)
240- >>> for bugtask in closed_elsewhere_tasks:
241- ... test_helper.assertBugTaskIsResolvedUpstream(bugtask)
242- ... print bugTaskInfo(bugtask)
243- 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG
244- 26 2 Ubuntu Blackhole Trash folder
245- 23 9 thunderbird (Ubuntu) Thunderbird crashes
246-
247-
248-Pass the open_upstream flag to include only bugtasks linked to those
249-watches or those upstream bugtasks that have the status "unconfirmed",
250-"needs info", "confirmed", "in progress" or "unknown". Note that a bug
251-may be associated with three or more bugtasks. If one upstream task
252-has a state associated with "open upstream", and another upstream task
253-has a state associated with "resolved upstream", the bug is included
254-in the results of the "open upstream" filter as well as the "resolved
255-upstream" filter.
256-
257-(In the examples below, the last bugtask is ellipsized because its ID
258- is generated here and therefore sampledata-dependent.)
259-
260- >>> params = BugTaskSearchParams(
261- ... open_upstream=True, orderby='id', user=None)
262- >>> open_elsewhere_tasks = ubuntu.searchTasks(params)
263- >>> for bugtask in open_elsewhere_tasks:
264- ... test_helper.assertBugTaskIsOpenUpstream(bugtask)
265- ... print bugTaskInfo(bugtask)
266- 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG
267- 26 2 Ubuntu Blackhole Trash folder
268- ... ... Ubuntu Copy, Cut and Delete operations should work on selections
269-
270-
271-We can also filter our search to include only bugs that are not known to
272-affect upstream, i.e., bugs that don't have an IUpstreamBugTask.
273-
274- >>> params = BugTaskSearchParams(
275- ... has_no_upstream_bugtask=True, orderby='id', user=None)
276- >>> tasks_with_no_upstreams = ubuntu.searchTasks(params)
277- >>> for bugtask in tasks_with_no_upstreams:
278- ... test_helper.assertShouldBeShownOnNoUpstreamTaskSearch(bugtask)
279- ... print bugTaskInfo(bugtask)
280- 25 10 linux-source-2.6.15 (Ubuntu) another test bug
281- ... ... Ubuntu Test nominated bug
282-
283-If we combine upstream-related filters, we get the union of the results
284-of the single filters.
285-
286- >>> params = BugTaskSearchParams(
287- ... has_no_upstream_bugtask=True, resolved_upstream=True,
288- ... orderby='id', user=None)
289- >>> tasks_with_no_upstreams = ubuntu.searchTasks(params)
290- >>> for bugtask in tasks_with_no_upstreams:
291- ... print bugTaskInfo(bugtask)
292- 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG
293- 26 2 Ubuntu Blackhole Trash folder
294- 23 9 thunderbird (Ubuntu) Thunderbird crashes
295- 25 10 linux-source-2.6.15 (Ubuntu) another test bug
296- ... ... Ubuntu Test nominated bug
297-
298-
299- >>> test_helper.tearDownBugsElsewhereTests()
300-
301-The search filter can also return bugs that are related to CVE reports:
302-
303- >>> from lp.bugs.interfaces.cve import ICveSet
304- >>> def getCves(bugtask):
305- ... bug, cve = getUtility(ICveSet).getBugCvesForBugTasks([bugtask])[0]
306- ... return cve.sequence
307- >>> params = BugTaskSearchParams(
308- ... has_cve=True, orderby='id', user=None)
309- >>> tasks_with_cves = ubuntu.searchTasks(params)
310- >>> for bugtask in tasks_with_cves:
311- ... print bugTaskInfo(bugtask), getCves(bugtask)
312- 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 1999-8979
313- 26 2 Ubuntu Blackhole Trash folder 1999-2345
314-
315-
316-== Searching by bug commenter ==
317-
318-The 'bug_commenter' parameter allows you to search bugtasks on which a certain
319-person has commented. No Privileges Person hasn't commented on any bugs, so no
320-bugs are found for him:
321-
322- >>> from lp.registry.interfaces.person import IPersonSet
323- >>> transaction.abort()
324-
325- >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
326- >>> no_priv_bug_commenter = BugTaskSearchParams(
327- ... user=None, bug_commenter=no_priv)
328- >>> found_bugtasks = bugtask_set.search(no_priv_bug_commenter)
329- >>> found_bugtasks.count()
330- 0
331-
332-If No Privileges Person comments on some bugs, those bugs can then be found by
333-a bug commenter search. There will be one bug task instance returned for each
334-bug target that the task is registered against, so in the test below three
335-comments will produce eight found bug tasks (three for bug 1, five for bug 2).
336-
337- >>> bug_one = getUtility(IBugSet).get(1)
338- >>> bug_one.newMessage(no_priv, 'No subject', 'some comment')
339- <Message at ...>
340-
341- >>> bug_two = getUtility(IBugSet).get(2)
342- >>> bug_two.newMessage(no_priv, 'No subject', 'another comment')
343- <Message at ...>
344-
345- >>> bug_two.newMessage(no_priv, 'No subject', 'yet another comment')
346- <Message at ...>
347-
348- >>> for (bug_id, target) in sorted((bugtask.bug.id, bugtask.bugtargetname)
349- ... for bugtask in found_bugtasks):
350- ... print bug_id, target
351- 1 firefox
352- 1 mozilla-firefox (Debian)
353- 1 mozilla-firefox (Ubuntu)
354- 2 Ubuntu Hoary
355- 2 mozilla-firefox (Debian Woody)
356- 2 mozilla-firefox (Debian)
357- 2 tomcat
358- 2 ubuntu
359-
360-If No Privileges Person reports a bug and does not comment on it, that bug
361-will not be included in the results returned by the bug commenter search.
362-
363- >>> from lp.bugs.interfaces.bug import CreateBugParams
364- >>> from lp.registry.interfaces.product import IProductSet
365-
366- >>> firefox = getUtility(IProductSet).getByName('firefox')
367- >>> firefox.createBug(
368- ... CreateBugParams(no_priv, "Some bug", "Some comment"))
369- <Bug at ...>
370-
371- >>> for (bug_id, target) in sorted((bugtask.bug.id, bugtask.bugtargetname)
372- ... for bugtask in found_bugtasks):
373- ... print bug_id, target
374- 1 firefox
375- 1 mozilla-firefox (Debian)
376- 1 mozilla-firefox (Ubuntu)
377- 2 Ubuntu Hoary
378- 2 mozilla-firefox (Debian Woody)
379- 2 mozilla-firefox (Debian)
380- 2 tomcat
381- 2 ubuntu
382-
383-
384-== Search for BugTasks assigned to milestones ==
385-
386-BugTaskSet.search() can return bugtasks associated with milestones.
387-No BugTask is associated yet with firefox milestone 1.0.
388-
389- >>> product_milestone = firefox.getMilestone('1.0')
390- >>> params = BugTaskSearchParams(milestone=product_milestone, user=None)
391- >>> milestone_tasks = bugtask_set.search(params)
392- >>> print milestone_tasks.count()
393- 0
394-
395-Similary, no BugTasks are associated with the project firexfox belongs to.
396-
397- >>> mozilla = firefox.project
398- >>> project_milestone = mozilla.getMilestone('1.0')
399- >>> params = BugTaskSearchParams(milestone=project_milestone, user=None)
400- >>> milestone_tasks = bugtask_set.search(params)
401- >>> print milestone_tasks.count()
402- 0
403-
404-When a BugTask is associated with a milestone, it is returned in a search
405-for bugs of this milestone.
406-
407- >>> bugtask = firefox.searchTasks(BugTaskSearchParams(user=None))[0]
408- >>> print bugTaskInfo(bugtask)
409- 2 1 Mozilla Firefox Firefox does not support SVG
410- >>> bugtask.milestone = product_milestone
411- >>> params = BugTaskSearchParams(milestone=product_milestone, user=None)
412- >>> milestone_tasks = bugtask_set.search(params)
413- >>> for bugtask in milestone_tasks:
414- ... print bugTaskInfo(bugtask)
415- 2 1 Mozilla Firefox Firefox does not support SVG
416-
417-This BugTask is also a BugTask of the milestone of the mozilla project.
418-
419- >>> params = BugTaskSearchParams(milestone=project_milestone, user=None)
420- >>> milestone_tasks = bugtask_set.search(params)
421- >>> for bugtask in milestone_tasks:
422- ... print bugTaskInfo(bugtask)
423- 2 1 Mozilla Firefox Firefox does not support SVG
424-
425-If a bug has one bugtask associated with a product and another bugtask
426-associated with a product series, and if both tasks are assigned to the
427-same milestone...
428-
429- >>> firefox_1_0 = firefox.getSeries("1.0")
430- >>> productseries_task = bugtask_set.createTask(
431- ... bug_one, no_priv, firefox_1_0)
432- >>> productseries_task.milestone = product_milestone
433- >>> print bugTaskInfo(productseries_task)
434- 40 1 Mozilla Firefox 1.0 Firefox does not support SVG
435-
436-...both of them are returned, by a search for bugs associated with the
437-product milestone...
438-
439- >>> params = BugTaskSearchParams(milestone=product_milestone, user=None)
440- >>> milestone_tasks = bugtask_set.search(params)
441- >>> for bugtask in milestone_tasks:
442- ... print bugTaskInfo(bugtask)
443- 2 1 Mozilla Firefox Firefox does not support SVG
444- 40 1 Mozilla Firefox 1.0 Firefox does not support SVG
445-
446-...as well as by a search for bugs associated with the project milestone.
447-
448- >>> params = BugTaskSearchParams(milestone=project_milestone, user=None)
449- >>> milestone_tasks = bugtask_set.search(params)
450- >>> for bugtask in milestone_tasks:
451- ... print bugTaskInfo(bugtask)
452- 2 1 Mozilla Firefox Firefox does not support SVG
453- 40 1 Mozilla Firefox 1.0 Firefox does not support SVG
454-
455-
456 == Bugs with partner packages ==
457
458 Bugs may also be targeted to partner packages. First turn "cdrkit" into
459@@ -535,6 +114,8 @@
460
461 >>> from zope.security.proxy import removeSecurityProxy
462 >>> from lp.soyuz.interfaces.component import IComponentSet
463+ >>> from lp.registry.interfaces.distribution import IDistributionSet
464+ >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
465 >>> proxied_cdrkit = ubuntu.getSourcePackage("cdrkit")
466 >>> cdrkit = removeSecurityProxy(proxied_cdrkit)
467 >>> cdrkit.component = getUtility(IComponentSet)['partner']
468@@ -550,6 +131,8 @@
469 We can file a bug against it and see that show up in a search:
470
471 >>> from lp.bugs.interfaces.bug import CreateBugParams
472+ >>> from lp.registry.interfaces.person import IPersonSet
473+ >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
474 >>> bug = cdrkit.createBug(
475 ... CreateBugParams(owner=no_priv, title='Bug to be fixed in trunk',
476 ... comment='Something'))
477@@ -558,239 +141,6 @@
478 1
479
480
481-== Searching by tags ==
482-
483-It is possible to search for bugs by their tags. Tags in the search
484-parameters can be combined using either ''any'' or ''all''.
485-
486-First, we create some test bugs.
487-
488- >>> firefox = getUtility(IProductSet).get(4)
489- >>> foobar = getUtility(IPersonSet).get(16)
490-
491-The first bug is tagged with both 'test-tag-1' and 'test-tag-2'.
492-
493- >>> params = CreateBugParams(
494- ... title="test bug a", comment="test bug a", owner=foobar)
495- >>> test_bug_a = firefox.createBug(params)
496- >>> test_bug_a.tags = ['test-tag-1', 'test-tag-2']
497-
498-The second bug is tagged with only 'test-tag-1'.
499-
500- >>> params = CreateBugParams(
501- ... title="test bug b", comment="test bug b", owner=foobar)
502- >>> test_bug_b = firefox.createBug(params)
503- >>> test_bug_b.tags = ['test-tag-1']
504-
505-Searching for bugs with any of the tags returns both of them.
506-
507- >>> from operator import attrgetter
508- >>> from lp.services.searchbuilder import all, any
509-
510- >>> def search_tasks_and_print_bugs(user=None, **args):
511- ... params = BugTaskSearchParams(user=user, **args)
512- ... tasks = firefox.searchTasks(params)
513- ... bugs = (task.bug for task in tasks)
514- ... bugs = sorted(bugs, key=attrgetter('id'))
515- ... for bug in bugs:
516- ... print "%s [%s]" % (bug.title, ", ".join(bug.tags))
517-
518- >>> search_tasks_and_print_bugs(
519- ... tag=any('test-tag-1', 'test-tag-2'))
520- test bug a [test-tag-1, test-tag-2]
521- test bug b [test-tag-1]
522-
523-Searching for bugs with all of the tags returns only test bug a.
524-
525- >>> search_tasks_and_print_bugs(
526- ... tag=all('test-tag-1', 'test-tag-2'))
527- test bug a [test-tag-1, test-tag-2]
528-
529-Search for the absence of a tag is possible by prefixing the tag name
530-with a minus.
531-
532- >>> search_tasks_and_print_bugs(tag=any('-test-tag-2'))
533- Firefox does not support SVG []
534- Reflow problems with complex page layouts [layout-test]
535- Firefox install instructions should be complete [doc]
536- Firefox crashes when Save As dialog for a nonexistent window is closed []
537- Some bug []
538- test bug b [test-tag-1]
539-
540-The any() and all() search combinators are taken into consideration
541-when searching for the absence of tags too. The following search says
542-"give me bugs that don't have the test-tag-2 tag set *OR* that don't
543-have the layout-test tag set". Only test-bug-1 is elimininated because
544-it has no tags other than those requested in the search.
545-
546- >>> search_tasks_and_print_bugs(
547- ... tag=any('-test-tag-1', '-test-tag-2'))
548- Firefox does not support SVG []
549- Reflow problems with complex page layouts [layout-test]
550- Firefox install instructions should be complete [doc]
551- Firefox crashes when Save As dialog for a nonexistent window is closed []
552- Some bug []
553- test bug b [test-tag-1]
554-
555-Whereas the following search says "give me bugs that don't have the
556-test-tag-2 tag set *AND* that don't have the layout-test tag set".
557-
558- >>> search_tasks_and_print_bugs(
559- ... tag=all('-test-tag-2', '-layout-test'))
560- Firefox does not support SVG []
561- Firefox install instructions should be complete [doc]
562- Firefox crashes when Save As dialog for a nonexistent window is closed []
563- Some bug []
564- test bug b [test-tag-1]
565-
566-Searching for the presence of any tags at all is also possible using a
567-wildcard. If prefixed with a minus it searches for the absence of
568-tags.
569-
570- >>> search_tasks_and_print_bugs(tag=all('*'))
571- Reflow problems with complex page layouts [layout-test]
572- Firefox install instructions should be complete [doc]
573- test bug a [test-tag-1, test-tag-2]
574- test bug b [test-tag-1]
575-
576- >>> search_tasks_and_print_bugs(tag=all('-*'))
577- Firefox does not support SVG []
578- Firefox crashes when Save As dialog for a nonexistent window is closed []
579- Some bug []
580-
581-Searching for the presence and absence of tags finds no
582-matches. Unsurprisingly.
583-
584- >>> search_tasks_and_print_bugs(tag=all('*', '-*'))
585-
586-Wildcards can be combined with non-wildcard tags. The following finds
587-all bugs with tags, but without test-tag-1:
588-
589- >>> search_tasks_and_print_bugs(tag=all('*', '-test-tag-1'))
590- Reflow problems with complex page layouts [layout-test]
591- Firefox install instructions should be complete [doc]
592-
593-The following is very similar; it finds all bugs with tags, *or*
594-without test-tag-1:
595-
596- >>> search_tasks_and_print_bugs(tag=any('*', '-test-tag-1'))
597- Firefox does not support SVG []
598- Reflow problems with complex page layouts [layout-test]
599- Firefox install instructions should be complete [doc]
600- Firefox crashes when Save As dialog for a nonexistent window is closed []
601- Some bug []
602- test bug a [test-tag-1, test-tag-2]
603- test bug b [test-tag-1]
604-
605-The following finds all untagged bugs and bugs with the doc tag.
606-
607- >>> search_tasks_and_print_bugs(tag=any('-*', 'doc'))
608- Firefox does not support SVG []
609- Firefox install instructions should be complete [doc]
610- Firefox crashes when Save As dialog for a nonexistent window is closed []
611- Some bug []
612-
613-
614-== Searching by date_closed ==
615-
616-It's possible to limit the search by date_closed, to get only bugs
617-closed after a certain date. greater_than is used to search for bugs
618-closed after a certain date.
619-
620- >>> import pytz
621- >>> from datetime import datetime, timedelta
622- >>> from lp.services.searchbuilder import greater_than
623- >>> product = factory.makeProduct()
624- >>> utc_now = datetime(2008, 9, 4, 12, 0, 0, tzinfo=pytz.timezone('UTC'))
625- >>> not_closed_bug = factory.makeBug(target=product, title="Not closed")
626- >>> bug_closed_a_day_ago = factory.makeBug(
627- ... target=product, date_closed=utc_now-timedelta(days=1),
628- ... title="Closed a day ago")
629- >>> bug_closed_a_week_ago = factory.makeBug(
630- ... target=product, date_closed=utc_now-timedelta(days=7),
631- ... title="Closed a week ago")
632-
633- >>> search_params = BugTaskSearchParams(
634- ... user=None, orderby="-date_closed",
635- ... date_closed=greater_than(utc_now))
636- >>> list(product.searchTasks(search_params))
637- []
638-
639- >>> search_params.date_closed = greater_than(utc_now - timedelta(days=2))
640- >>> for bug_task in product.searchTasks(search_params):
641- ... print bug_task.bug.title
642- Closed a day ago
643-
644-
645-== Searching for bug with attachments ==
646-
647-It's possible to search for bugs with an attachment of a certain type.
648-
649- >>> from StringIO import StringIO
650- >>> from lp.services.librarian.interfaces import ILibraryFileAliasSet
651- >>> from lp.services.messages.interfaces.message import IMessageSet
652- >>> from lp.bugs.interfaces.bugattachment import (
653- ... BugAttachmentType,
654- ... IBugAttachmentSet,
655- ... )
656- >>> product = factory.makeProduct()
657- >>> patch_bug = factory.makeBug(target=product)
658- >>> filecontent = 'Some diff data'
659- >>> filealias = getUtility(ILibraryFileAliasSet).create(
660- ... name='patch.diff', size=len(filecontent),
661- ... file=StringIO(filecontent), contentType='text/plain')
662- >>> message = getUtility(IMessageSet).fromText(
663- ... subject="title", content="added a patch.")
664- >>> attachmentset = getUtility(IBugAttachmentSet)
665- >>> attachment = attachmentset.create(
666- ... bug=patch_bug, filealias=filealias, title='Patch',
667- ... message=message, attach_type=BugAttachmentType.PATCH)
668- >>> patch_bug.attachments.count()
669- 1
670-
671-We've added an attachment to our new bug with an attachment type
672-PATCH. Searching for bugs with that attachment type we get one result.
673-
674- >>> search_params = BugTaskSearchParams(
675- ... user=None, attachmenttype=BugAttachmentType.PATCH)
676- >>> product.searchTasks(search_params).count()
677- 1
678-
679- >>> filecontent = 'Some more diff data'
680- >>> filealias = getUtility(ILibraryFileAliasSet).create(
681- ... name='patch.diff', size=len(filecontent),
682- ... file=StringIO(filecontent), contentType='text/plain')
683- >>> message = getUtility(IMessageSet).fromText(
684- ... subject="title", content="added another patch.")
685- >>> attachmentset = getUtility(IBugAttachmentSet)
686- >>> attachment = attachmentset.create(
687- ... bug=patch_bug, filealias=filealias, title='Patch 2',
688- ... message=message, attach_type=BugAttachmentType.PATCH)
689- >>> patch_bug.attachments.count()
690- 2
691-
692-We've added another patch to the bug. Searching for bugs with patch
693-attachments still returns a single result, since even though a new
694-attachment was added, there is still only one bug with attachments.
695-
696- >>> product.searchTasks(search_params).count()
697- 1
698-
699-== Searching for bugs affecting a user ==
700-
701-We can search for bugs which a user marked as affecting them.
702-
703- >>> affecting_bug = factory.makeBug(title='A bug affecting a user')
704- >>> affected_user = factory.makePerson(name='affected-user')
705- >>> affecting_bug.markUserAffected(affected_user)
706- >>> target = affecting_bug.bugtasks[0].target
707- >>> affecting_tasks = target.searchTasks(
708- ... None, user=None, affected_user=affected_user)
709- >>> for task in affecting_tasks:
710- ... print task.bug.title
711- A bug affecting a user
712-
713-
714 == Searching for bugs related to hardware ==
715
716 We can search for bugs which are related to a given hardware device or
717@@ -825,7 +175,7 @@
718 9 Foo Bar
719 10 Foo Bar
720 2 Sample Person
721- 19 No Privileges Person
722+ 16 No Privileges Person
723
724 Similary, we can search for device drivers appearing in HWDB submissions
725 of a bug reporter.
726@@ -871,6 +221,8 @@
727 The PCI device (0x10de, 0x0455) is not controlled in any HWDB submission
728 by the sd driver, so we'll get an empty result set for this query.
729
730+ >>> from lp.registry.interfaces.product import IProductSet
731+ >>> firefox = getUtility(IProductSet).getByName('firefox')
732 >>> search_params = BugTaskSearchParams(
733 ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de',
734 ... hardware_product_id='0x0455', hardware_driver_name='sd',
735@@ -880,7 +232,8 @@
736
737 We can also search for device owners which are subscribed to a bug.
738
739- >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
740+ >>> sample_person = getUtility(IPersonSet).getByEmail(
741+ ... 'test@canonical.com')
742 >>> search_params = BugTaskSearchParams(
743 ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de',
744 ... hardware_product_id='0x0455',
745@@ -909,14 +262,14 @@
746 >>> from lp.hardwaredb.interfaces.hwdb import IHWSubmissionSet
747 >>> hw_submission = getUtility(IHWSubmissionSet).getBySubmissionKey(
748 ... 'sample-submission')
749- >>> bug_19 = getUtility(IBugSet).get(19)
750- >>> bug_19.linkHWSubmission(hw_submission)
751+ >>> bug_16 = getUtility(IBugSet).get(16)
752+ >>> bug_16.linkHWSubmission(hw_submission)
753 >>> search_params = BugTaskSearchParams(
754 ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de',
755 ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True)
756 >>> for bugtask in ubuntu.searchTasks(search_params):
757 ... print bugtask.bug.id
758- 19
759+ 16
760
761 If a device appears in a private submission, related bugs are shown
762 only if the user running the request is the owner of the submission
763@@ -930,14 +283,15 @@
764 ... hardware_is_linked_to_bug=True)
765 >>> for bugtask in ubuntu.searchTasks(search_params):
766 ... print bugtask.bug.id
767- 19
768+ 16
769
770+ >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
771 >>> search_params = BugTaskSearchParams(
772 ... user=foo_bar, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de',
773 ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True)
774 >>> for bugtask in ubuntu.searchTasks(search_params):
775 ... print bugtask.bug.id
776- 19
777+ 16
778
779 Other users cannot see that a bug is related to a device from a
780 private submission.
781@@ -954,55 +308,6 @@
782 >>> ubuntu.searchTasks(search_params).count()
783 0
784
785-== Searching for bugs affecting me ==
786-
787-The user searching for bugs can search for bugs affecting him.
788-
789-We search for bugs affecting foo_bar, then check that all the results
790-return True for isUserAffected(foo_bar).
791-
792- >>> search_params = BugTaskSearchParams(
793- ... user=foo_bar, affects_me=True)
794- >>> print reduce(
795- ... lambda x, y: x and y,
796- ... [task.bug.isUserAffected(foo_bar)
797- ... for task in firefox.searchTasks(search_params)])
798- True
799-
800-
801-== Searching for bugs linked to branches ==
802-
803-We can search for bugs having branches linked to them.
804-
805- >>> from lp.bugs.interfaces.bugtasksearch import BugBranchSearch
806- >>> search_params = BugTaskSearchParams(
807- ... user=None, linked_branches=BugBranchSearch.BUGS_WITH_BRANCHES)
808- >>> for task in firefox.searchTasks(search_params):
809- ... print task.bug.id, task.bug.linked_branches.count()
810- 4 2
811- 5 1
812-
813-Similarly, we can search for bugs that do not have any linked branches.
814-
815- >>> search_params = BugTaskSearchParams(
816- ... user=None, linked_branches=BugBranchSearch.BUGS_WITHOUT_BRANCHES)
817- >>> for task in firefox.searchTasks(search_params):
818- ... print task.bug.id, task.bug.linked_branches.count()
819- 1 0
820- 6 0
821- 18 0
822- 20 0
823- 21 0
824-
825-And we can search for bugs linked to a specific branch.
826-
827- >>> search_params = BugTaskSearchParams(
828- ... user=None, linked_branches=1)
829- >>> for task in firefox.searchTasks(search_params):
830- ... print task.bug.id
831- 4
832- 5
833-
834
835 == Ordering search results ==
836
837@@ -1015,6 +320,11 @@
838
839 Here is the list of bugs for Ubuntu.
840
841+ >>> def bugTaskInfo(bugtask):
842+ ... return '%i %i %s %s' % (
843+ ... bugtask.id, bugtask.bug.id, bugtask.bugtargetdisplayname,
844+ ... bugtask.bug.title)
845+
846 >>> params = BugTaskSearchParams(
847 ... orderby='-number_of_duplicates', user=None)
848 >>> ubuntu_tasks = ubuntu.searchTasks(params)
849@@ -1024,7 +334,7 @@
850 23 9 thunderbird (Ubuntu) Thunderbird crashes
851 25 10 linux-source-2.6.15 (Ubuntu) another test bug
852 26 2 Ubuntu Blackhole Trash folder
853- 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk
854+ 36 16 cdrkit (Ubuntu) Bug to be fixed in trunk
855
856 None of these bugs have any duplicates.
857
858@@ -1041,7 +351,8 @@
859 >>> bug_ten.markAsDuplicate(bug_nine)
860 >>> flush_database_updates()
861
862-Searching again reveals bug #9 at the top of the list, since it now has a duplicate.
863+Searching again reveals bug #9 at the top of the list, since it now has
864+a duplicate.
865
866 >>> ubuntu_tasks = ubuntu.searchTasks(params)
867 >>> for bugtask in ubuntu_tasks:
868@@ -1050,7 +361,7 @@
869 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG
870 25 10 linux-source-2.6.15 (Ubuntu) another test bug
871 26 2 Ubuntu Blackhole Trash folder
872- 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk
873+ 36 16 cdrkit (Ubuntu) Bug to be fixed in trunk
874
875
876 === Ordering by number of comments ===
877@@ -1066,11 +377,11 @@
878 ... bug = bugtask.bug
879 ... print '%s %s [%s comments]' % (
880 ... bug.id, bug.title, bug.message_count)
881- 2 Blackhole Trash folder [5 comments]
882- 1 Firefox does not support SVG [3 comments]
883+ 2 Blackhole Trash folder [3 comments]
884+ 1 Firefox does not support SVG [2 comments]
885 10 another test bug [2 comments]
886 9 Thunderbird crashes [1 comments]
887- 19 Bug to be fixed in trunk [1 comments]
888+ 16 Bug to be fixed in trunk [1 comments]
889
890
891 === Ordering by bug heat ===
892@@ -1090,7 +401,7 @@
893 ... bug = bugtask.bug
894 ... print '%s %s [heat: %s]' % (
895 ... bug.id, bug.title, bug.heat)
896- 19 Bug to be fixed in trunk [heat: 19]
897+ 16 Bug to be fixed in trunk [heat: 16]
898 10 another test bug [heat: 10]
899 9 Thunderbird crashes [heat: 9]
900 2 Blackhole Trash folder [heat: 2]
901@@ -1111,7 +422,7 @@
902 >>> ubuntu_tasks = ubuntu.searchTasks(params)
903 >>> for bugtask in ubuntu_tasks:
904 ... print bugTaskInfo(bugtask)
905- 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk
906+ 36 16 cdrkit (Ubuntu) Bug to be fixed in trunk
907 26 2 Ubuntu Blackhole Trash folder
908 25 10 linux-source-2.6.15 (Ubuntu) another test bug
909 23 9 thunderbird (Ubuntu) Thunderbird crashes
910@@ -1119,6 +430,7 @@
911
912 If we add a patch attachment to bug 2 and bug 10, they are listed first.
913
914+ >>> bug_two = getUtility(IBugSet).get(2)
915 >>> patch_attachment_bug_2 = factory.makeBugAttachment(
916 ... bug=bug_two, is_patch=True)
917 >>> transaction.commit()
918@@ -1131,237 +443,6 @@
919 ... print bugTaskInfo(bugtask)
920 26 2 Ubuntu Blackhole Trash folder
921 25 10 linux-source-2.6.15 (Ubuntu) another test bug
922- 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk
923+ 36 16 cdrkit (Ubuntu) Bug to be fixed in trunk
924 23 9 thunderbird (Ubuntu) Thunderbird crashes
925 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG
926-
927-
928-== Searching using a flat interface ==
929-
930-An alternative, simplified interface for searching bug tasks is available
931-by passing all search parameters as function arguments to searchTasks.
932-This interface corresponds to the search options available from the web
933-search form and the public API.
934-
935- >>> def print_bugtasks(bugtasks):
936- ... for bugtask in bugtasks:
937- ... print '%s %s %s %s %s' % (
938- ... bugtask.bug.id, bugtask.bugtargetdisplayname,
939- ... bugtask.bug.title, bugtask.status.title.upper(),
940- ... bugtask.importance.title.upper())
941-
942-
943-When we call the method on Ubuntu, without any parameters, the result is
944-identical to calling searchTasks.
945-
946- >>> print_bugtasks(ubuntu.searchTasks(None, user=None))
947- 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM
948- 9 thunderbird (Ubuntu) Thunderbird crashes CONFIRMED MEDIUM
949- 2 Ubuntu Blackhole Trash folder NEW MEDIUM
950- 19 cdrkit (Ubuntu) Bug to be fixed in trunk NEW UNDECIDED
951-
952-If we want to restrict the search using certain parameters pass them to
953-the function directly. Here we search Ubuntu again, but only bugs for
954-the firefox package.
955-
956- >>> from lp.registry.interfaces.sourcepackagename import (
957- ... ISourcePackageNameSet)
958- >>> source_package_name_set = getUtility(ISourcePackageNameSet)
959- >>> firefox_source_package = source_package_name_set['mozilla-firefox']
960- >>> print_bugtasks(ubuntu.searchTasks(
961- ... None, user=None, sourcepackagename=firefox_source_package))
962- 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM
963-
964-Or search for bugs on a distribution source package directly.
965-
966- >>> print_bugtasks(ubuntu_firefox.searchTasks(None, user=None))
967- 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM
968-
969-Or we can search a certain milestone (only getting bugs targeted to it).
970-
971- >>> firefox_milestone_1 = firefox.getMilestone('1.0')
972- >>> print_bugtasks(firefox_milestone_1.searchTasks(None, user=None))
973- 1 Mozilla Firefox Firefox does not support SVG NEW LOW
974- 1 Mozilla Firefox 1.0 Firefox does not support SVG NEW UNDECIDED
975-
976-We can restrict our search for firefox bugs with a text search.
977-
978- >>> print_bugtasks(firefox.searchTasks(
979- ... None, user=None, search_text=u'instructions'))
980- 5 Mozilla Firefox Firefox install instructions should be complete NEW CRITICAL
981-
982-Or restrict our search (over the mozilla project this time) to a list of
983-relevant importance values.
984-
985- >>> print_bugtasks(mozilla.searchTasks(
986- ... None, user=None,
987- ... importance=[BugTaskImportance.LOW, BugTaskImportance.MEDIUM]))
988- 4 Mozilla Firefox Reflow problems with complex page layouts NEW MEDIUM
989- 1 Mozilla Firefox Firefox does not support SVG NEW LOW
990-
991-
992-== Searching by structural subscriber ==
993-
994-The 'structural_subscriber' search parameter allows one to search all the bug
995-tasks to which a person is structurally subscribed. A person can be a
996-structural subscriber for a product, a product series, a project, a milestone,
997-a distribution, a distribution series and a distribution source package. No
998-Privileges Person isn't a structural subscriber, so no bug tasks are found:
999-
1000- >>> from lp.registry.interfaces.person import IPersonSet
1001- >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
1002- >>> no_priv_struct_sub = BugTaskSearchParams(
1003- ... user=None, structural_subscriber=no_priv)
1004- >>> found_bugtasks = bugtask_set.search(no_priv_struct_sub)
1005- >>> found_bugtasks.count()
1006- 0
1007-
1008-Create a new person and make them a subscriber to all Firefox (product) bug
1009-reports. Subsequently, we confirm that they are subscribed to all of the
1010-Firefox bug tasks.
1011-
1012- >>> product_struct_subber = factory.makePerson(
1013- ... name='product-struct-subber')
1014- >>> firefox.addBugSubscription(product_struct_subber,
1015- ... product_struct_subber)
1016- <...StructuralSubscription object at ...>
1017- >>> product_struct_sub_search = BugTaskSearchParams(
1018- ... user=None, structural_subscriber=product_struct_subber)
1019- >>> found_bugtasks = bugtask_set.search(product_struct_sub_search)
1020- >>> found_bugtasks.count()
1021- 7
1022-
1023-Create a new person and subscribe them to all of the bug tasks for a product
1024-series. We then test to see that they are subscribed to all of the bug tasks
1025-for the product series in which they are interested.
1026-
1027- >>> product_series = firefox.getSeries('1.0')
1028- >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False)
1029- >>> series_tasks = product_series.searchTasks(all_targeted)
1030- >>> series_struct_subber = factory.makePerson(
1031- ... name='series-struct-subber')
1032- >>> product_series.addBugSubscription(series_struct_subber,
1033- ... series_struct_subber)
1034- <...StructuralSubscription object at ...>
1035- >>> series_struct_sub_search = BugTaskSearchParams(
1036- ... user=None, structural_subscriber=series_struct_subber)
1037- >>> found_bugtasks = bugtask_set.search(series_struct_sub_search)
1038- >>> found_bugtasks.count()
1039- 2
1040-
1041-Create a new product which will be a part of a project group. A bug is
1042-created for the product which should then show up for structural subscribers
1043-of the project group. Then a new person is created who is subscribed to all
1044-of the bug reports about the project. Search for bug tasks that this new
1045-person is subscribed.
1046-
1047- >>> product = factory.makeProduct()
1048- >>> bug = factory.makeBug(target=product)
1049- >>> project = factory.makeProject()
1050- >>> product.project = project
1051- >>> project_struct_subber = factory.makePerson(
1052- ... name='project-struct-subber')
1053- >>> project.addBugSubscription(project_struct_subber,
1054- ... project_struct_subber)
1055- <...StructuralSubscription object at ...>
1056- >>> project_struct_sub_search = BugTaskSearchParams(
1057- ... user=None, structural_subscriber=project_struct_subber)
1058- >>> found_bugtasks = bugtask_set.search(project_struct_sub_search)
1059- >>> found_bugtasks.count()
1060- 1
1061-
1062-We will also subscribe this project subscriber to a product that is a part of
1063-the project and ensure that duplicate bug tasks do not appear in the search
1064-results.
1065-
1066- >>> product2 = factory.makeProduct()
1067- >>> bug = factory.makeBug(target=product2)
1068- >>> product2.project = project
1069- >>> product2.addBugSubscription(project_struct_subber,
1070- ... project_struct_subber)
1071- <...StructuralSubscription object at ...>
1072- >>> project_struct_sub_search = BugTaskSearchParams(
1073- ... user=None, structural_subscriber=project_struct_subber)
1074- >>> found_bugtasks = bugtask_set.search(project_struct_sub_search)
1075- >>> found_bugtasks.count()
1076- 2
1077-
1078-Create a new person and subscribe them to all of bug tasks targeted to a
1079-milestone. We then test to see that they are subscribed to all of the
1080-bug tasks for the milestone in which they are interested.
1081-
1082- >>> milestone_struct_subber = factory.makePerson(
1083- ... name='milestone-struct-subber')
1084- >>> product_milestone.addBugSubscription(milestone_struct_subber,
1085- ... milestone_struct_subber)
1086- <...StructuralSubscription object at ...>
1087- >>> milestone_struct_sub_search = BugTaskSearchParams(
1088- ... user=None, structural_subscriber=milestone_struct_subber)
1089- >>> found_bugtasks = bugtask_set.search(milestone_struct_sub_search)
1090- >>> found_bugtasks.count()
1091- 2
1092-
1093-Create another new person and subscribe them to all the Ubuntu bug reports -
1094-crazy I know. Then test to see that this poor person is subscribed to all
1095-bugs with an Ubuntu bug task.
1096-
1097- >>> distro_struct_subber = factory.makePerson(
1098- ... name='distro-struct-subber')
1099- >>> ubuntu.addBugSubscription(distro_struct_subber,
1100- ... distro_struct_subber)
1101- <...StructuralSubscription object at ...>
1102- >>> distro_struct_sub_search = BugTaskSearchParams(
1103- ... user=None, structural_subscriber=distro_struct_subber)
1104- >>> found_bugtasks = bugtask_set.search(distro_struct_sub_search)
1105- >>> found_bugtasks.count()
1106- 5
1107-
1108-Create a new person who will only be subscribed to an Ubuntu series, which is
1109-something more reasonable than all of Ubuntu. Test to ensure that this person
1110-is subscribed to all of the bug tasks about that distro series.
1111-
1112- >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
1113- >>> hoary = ubuntu.getSeries('hoary')
1114- >>> distro_series_struct_subber = factory.makePerson(
1115- ... name='distro-series-struct-subber')
1116- >>> hoary.addBugSubscription(distro_series_struct_subber,
1117- ... distro_series_struct_subber)
1118- <...StructuralSubscription object at ...>
1119- >>> distro_series_struct_sub_search = BugTaskSearchParams(
1120- ... user=None, structural_subscriber=distro_series_struct_subber)
1121- >>> found_bugtasks = bugtask_set.search(distro_series_struct_sub_search)
1122- >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False)
1123- >>> hoary_bugtasks = hoary.searchTasks(all_targeted)
1124- >>> found_bugtasks.count() == hoary_bugtasks.count()
1125- True
1126-
1127-Create a new person and make them a subscriber to all Ubuntu Firefox (a
1128-distribution source package) bug reports. Test to see that the new person is
1129-subscribed to all of the Ubuntu Firefox bug tasks.
1130-
1131- >>> package_struct_subber = factory.makePerson(
1132- ... name='package-struct-subber')
1133- >>> ubuntu_firefox.addBugSubscription(package_struct_subber,
1134- ... package_struct_subber)
1135- <...StructuralSubscription object at ...>
1136- >>> package_struct_sub_search = BugTaskSearchParams(
1137- ... user=None, structural_subscriber=package_struct_subber)
1138- >>> found_bugtasks = bugtask_set.search(package_struct_sub_search)
1139- >>> found_bugtasks.count()
1140- 1
1141-
1142-We'll also subscribe the person who is currently subscribed to a package's bug
1143-reports, package_struct_subber, to the bug reports of a product series to
1144-ensure that the structural_subscriber search is returning the set of both bug
1145-tasks.
1146-
1147- >>> product_series.addBugSubscription(package_struct_subber,
1148- ... package_struct_subber)
1149- <...StructuralSubscription object at ...>
1150- >>> package_struct_sub_search = BugTaskSearchParams(
1151- ... user=None, structural_subscriber=package_struct_subber)
1152- >>> found_bugtasks = bugtask_set.search(package_struct_sub_search)
1153- >>> combined_bugtasks_count = (ubuntu_firefox_bugs.count () +
1154- ... series_tasks.count())
1155- >>> found_bugtasks.count() == combined_bugtasks_count
1156- True
1157
1158=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
1159--- lib/lp/bugs/model/bugtasksearch.py 2012-09-24 05:17:00 +0000
1160+++ lib/lp/bugs/model/bugtasksearch.py 2012-09-28 22:06:42 +0000
1161@@ -87,11 +87,9 @@
1162 from lp.services.database.lpstorm import IStore
1163 from lp.services.database.sqlbase import sqlvalues
1164 from lp.services.database.stormexpr import (
1165- Array,
1166 ArrayAgg,
1167 ArrayIntersects,
1168 get_where_for_reference,
1169- NullCount,
1170 Unnest,
1171 )
1172 from lp.services.propertycache import get_property_cache
1173@@ -478,8 +476,6 @@
1174 BugSubscription.person == params.subscriber))
1175
1176 if params.structural_subscriber is not None:
1177- # See bug 787294 for the story that led to the query elements
1178- # below. Please change with care.
1179 with_clauses.append(
1180 '''ss as (SELECT * from StructuralSubscription
1181 WHERE StructuralSubscription.subscriber = %s)'''
1182@@ -488,79 +484,65 @@
1183 class StructuralSubscriptionCTE(StructuralSubscription):
1184 __storm_table__ = 'ss'
1185
1186- join_tables.append(
1187- (Product, LeftJoin(Product, And(
1188- BugTaskFlat.product_id == Product.id,
1189- Product.active))))
1190- ProductSub = ClassAlias(StructuralSubscriptionCTE)
1191- join_tables.append((
1192- ProductSub,
1193- LeftJoin(
1194- ProductSub,
1195- BugTaskFlat.product_id == ProductSub.productID)))
1196- ProductSeriesSub = ClassAlias(StructuralSubscriptionCTE)
1197- join_tables.append((
1198- ProductSeriesSub,
1199- LeftJoin(
1200- ProductSeriesSub,
1201- BugTaskFlat.productseries_id ==
1202- ProductSeriesSub.productseriesID)))
1203- ProjectSub = ClassAlias(StructuralSubscriptionCTE)
1204- join_tables.append((
1205- ProjectSub,
1206- LeftJoin(
1207- ProjectSub,
1208- Product.projectID == ProjectSub.projectID)))
1209- DistributionSub = ClassAlias(StructuralSubscriptionCTE)
1210- join_tables.append((
1211- DistributionSub,
1212- LeftJoin(
1213- DistributionSub,
1214- And(BugTaskFlat.distribution_id ==
1215- DistributionSub.distributionID,
1216- Or(
1217- DistributionSub.sourcepackagenameID ==
1218- BugTaskFlat.sourcepackagename_id,
1219- DistributionSub.sourcepackagenameID == None)))))
1220- if params.distroseries is not None:
1221- parent_distro_id = params.distroseries.distributionID
1222- else:
1223- parent_distro_id = 0
1224- DistroSeriesSub = ClassAlias(StructuralSubscriptionCTE)
1225- join_tables.append((
1226- DistroSeriesSub,
1227- LeftJoin(
1228- DistroSeriesSub,
1229- Or(BugTaskFlat.distroseries_id ==
1230- DistroSeriesSub.distroseriesID,
1231- # There is a mismatch between BugTask and
1232- # StructuralSubscription. SS does not support
1233- # distroseries. This clause works because other
1234- # joins ensure the match bugtask is the right
1235- # series.
1236- And(parent_distro_id == DistroSeriesSub.distributionID,
1237- BugTaskFlat.sourcepackagename_id ==
1238- DistroSeriesSub.sourcepackagenameID)))))
1239- MilestoneSub = ClassAlias(StructuralSubscriptionCTE)
1240- join_tables.append((
1241- MilestoneSub,
1242- LeftJoin(
1243- MilestoneSub,
1244- BugTaskFlat.milestone_id == MilestoneSub.milestoneID)))
1245- extra_clauses.append(
1246- NullCount(Array(
1247- ProductSub.id, ProductSeriesSub.id, ProjectSub.id,
1248- DistributionSub.id, DistroSeriesSub.id, MilestoneSub.id)) < 6)
1249- has_duplicate_results = True
1250+ SS = ClassAlias(StructuralSubscriptionCTE)
1251+ # Milestones apply to all structural subscription searches.
1252+ ss_clauses = [
1253+ In(BugTaskFlat.milestone_id, Select(SS.milestoneID, tables=[SS]))]
1254+ if (params.project is None
1255+ and params.product is None and params.productseries is None):
1256+ # This search is *not* contrained to project related bugs, so
1257+ # include distro, distroseries, DSP and SP subscriptions.
1258+ ss_clauses.append(In(
1259+ BugTaskFlat.distribution_id,
1260+ Select(SS.distributionID, tables=[SS],
1261+ where=(SS.sourcepackagenameID == None))))
1262+ ss_clauses.append(In(
1263+ Row(BugTaskFlat.distribution_id,
1264+ BugTaskFlat.sourcepackagename_id),
1265+ Select((SS.distributionID, SS.sourcepackagenameID),
1266+ tables=[SS])))
1267+ ss_clauses.append(In(
1268+ BugTaskFlat.distroseries_id,
1269+ Select(SS.distroseriesID, tables=[SS],
1270+ where=(SS.sourcepackagenameID == None))))
1271+ # Users expect to find their DSP subscriptions when searching
1272+ # distroseries. We only include these when we need to.
1273+ if params.distroseries is not None:
1274+ distroseries_id = params.distroseries.id
1275+ parent_distro_id = params.distroseries.distributionID
1276+ else:
1277+ distroseries_id = 0
1278+ parent_distro_id = 0
1279+ ss_clauses.append(In(
1280+ Row(BugTaskFlat.distroseries_id,
1281+ BugTaskFlat.sourcepackagename_id),
1282+ Select((distroseries_id, SS.sourcepackagenameID), tables=[SS],
1283+ where=And(
1284+ SS.distributionID == parent_distro_id,
1285+ SS.sourcepackagenameID != None))))
1286+ if params.distribution is None and params.distroseries is None:
1287+ # This search is *not* contrained to distro related bugs so
1288+ # include products, productseries, and project group subscriptions.
1289+ project_match = True
1290+ if params.project is not None:
1291+ project_match = Product.project == params.project
1292+ ss_clauses.append(In(
1293+ BugTaskFlat.product_id,
1294+ Select(SS.productID, tables=[SS])))
1295+ ss_clauses.append(In(
1296+ BugTaskFlat.productseries_id,
1297+ Select(SS.productseriesID, tables=[SS])))
1298+ ss_clauses.append(In(
1299+ BugTaskFlat.product_id,
1300+ Select(Product.id, tables=[SS, Product],
1301+ where=And(
1302+ SS.projectID == Product.projectID,
1303+ project_match,
1304+ Product.active))))
1305+ extra_clauses.append(Or(*ss_clauses))
1306
1307- # Remove bugtasks from deactivated products, if necessary.
1308- # We don't have to do this if
1309- # 1) We're searching on bugtasks for a specific product
1310- # 2) We're searching on bugtasks for a specific productseries
1311- # 3) We're searching on bugtasks for a distribution
1312- # 4) We're searching for bugtasks for a distroseries
1313- # because in those instances we don't have arbitrary products which
1314- # may be deactivated showing up in our search.
1315+ # Remove bugtasks from deactivated products. This is needed for searches
1316+ # where people or project groups are the context.
1317 if (params.product is None and
1318 params.distribution is None and
1319 params.productseries is None and
1320@@ -618,15 +600,10 @@
1321 if tag_clause is not None:
1322 extra_clauses.append(tag_clause)
1323
1324- # XXX Tom Berger 2008-02-14:
1325- # We use StructuralSubscription to determine
1326- # the bug supervisor relation for distribution source
1327- # packages, following a conversion to use this object.
1328- # We know that the behaviour remains the same, but we
1329- # should change the terminology, or re-instate
1330- # PackageBugSupervisor, since the use of this relation here
1331- # is not for subscription to notifications.
1332- # See bug #191809
1333+ # XXX sinzui 2012-09-26:
1334+ # This uses StructuralSubscription to assume a bug supervisor relationship
1335+ # for distribution source packages to preserve historical behaviour.
1336+ # This also duplicates params.structural_subscriber code and behaviour.
1337 if params.bug_supervisor:
1338 extra_clauses.append(Or(
1339 In(
1340
1341=== modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py'
1342--- lib/lp/bugs/model/tests/test_bugtasksearch.py 2012-09-21 02:03:56 +0000
1343+++ lib/lp/bugs/model/tests/test_bugtasksearch.py 2012-09-28 22:06:42 +0000
1344@@ -2377,6 +2377,29 @@
1345 self.assertContentEqual(bug1.bugtasks, tasks)
1346
1347
1348+class TargetLessTestCase(TestCaseWithFactory):
1349+ """Test that do not call setTarget() in the BugTaskSearchParams."""
1350+
1351+ layer = DatabaseFunctionalLayer
1352+
1353+ def test_project_group_structural_subscription(self):
1354+ # Search results can be limited to bugs without a bug target to which
1355+ # a given person has a structural subscription.
1356+ subscriber = self.factory.makePerson()
1357+ product = self.factory.makeProduct()
1358+ self.factory.makeBug(target=product)
1359+ with person_logged_in(product.owner):
1360+ project_group = self.factory.makeProject(owner=product.owner)
1361+ product.project = project_group
1362+ with person_logged_in(subscriber):
1363+ project_group.addBugSubscription(subscriber, subscriber)
1364+ params = BugTaskSearchParams(
1365+ user=None, structural_subscriber=subscriber)
1366+ bugtask_set = getUtility(IBugTaskSet)
1367+ found_bugtasks = bugtask_set.search(params)
1368+ self.assertEqual(1, found_bugtasks.count())
1369+
1370+
1371 class BaseGetBugPrivacyFilterTermsTests:
1372
1373 layer = DatabaseFunctionalLayer