4
The BugSummary table contains summaries of bug counts. It contains one
5
row for every unique public BugTask targetting:
17
First we should setup some helpers to use in the examples. These will
18
let us dump the BugSummary table in a readable format.
20
--------------------------------------------------------
21
prod ps dist ds spn tag mile status vis #
22
--------------------------------------------------------
24
The columns are product, productseries, distribution, distroseries,
25
sourcepackagename, tag, milestone, status, viewed_by and the count.
26
viewed_by is a team reference and used to query private bug counts.
28
>>> from canonical.launchpad.interfaces.lpstorm import IMasterStore
29
>>> from lp.bugs.interfaces.bugtask import BugTaskStatus
30
>>> from lp.bugs.model.bugsummary import BugSummary
31
>>> from lp.testing import login_celebrity
32
>>> me = login_celebrity("admin")
33
>>> store = IMasterStore(BugSummary)
35
>>> def name(object_or_none):
36
... if object_or_none is None:
38
... return object_or_none.name
40
>>> def print_result(bugsummary_resultset):
41
... # First, flush and invalidate the cache so we see the effects
42
... # of the underlying database triggers. Normally you don't want
43
... # to bother with this as you are only interested in counts of
44
... # bugs created in previous transactions.
46
... store.invalidate()
48
... # And rollup the BugSummaryJournal into BugSummary
49
... # so all the records are in one place.
50
... store.execute("SELECT bugsummary_rollup_journal()")
52
... # Make sure our results are in a consistent order.
53
... ordered_results = bugsummary_resultset.order_by(
54
... BugSummary.product_id, BugSummary.productseries_id,
55
... BugSummary.distribution_id, BugSummary.distroseries_id,
56
... BugSummary.sourcepackagename_id, BugSummary.tag,
57
... BugSummary.milestone_id, BugSummary.status,
58
... BugSummary.viewed_by_id, BugSummary.id)
59
... fmt = "%-4s %-4s %-4s %-4s %-5s %-3s %-4s %-12s %-4s %3s"
61
... 'prod', 'ps', 'dist', 'ds', 'spn', 'tag', 'mile',
62
... 'status', 'vis', '#')
63
... print "-" * len(header)
65
... print "-" * len(header)
66
... for bugsummary in ordered_results:
68
... name(bugsummary.product),
69
... name(bugsummary.productseries),
70
... name(bugsummary.distribution),
71
... name(bugsummary.distroseries),
72
... name(bugsummary.sourcepackagename),
73
... bugsummary.tag or 'x',
74
... name(bugsummary.milestone),
75
... bugsummary.status,
76
... name(bugsummary.viewed_by),
78
... print " " * (len(header) - 4),
80
... sum = bugsummary_resultset.sum(BugSummary.count)
81
... print " " * (len(header) - 4),
84
>>> def print_find(*bs_query_args, **bs_query_kw):
85
... resultset = store.find(BugSummary, *bs_query_args, **bs_query_kw)
86
... print_result(resultset)
89
/!\ A Note About Privacy in These Examples
90
------------------------------------------
92
All the examples, except for the ones in the Privacy section, are
93
dealing with public bugs only. This is why they all are using
94
''BugSummary.viewed_by == None'' in their queries.
96
To count private bugs, these queries need to join with the
97
TeamParticipation table as detailed in the Privacy section.
103
We can query for how many bugs are targeted to a product.
105
>>> prod_a = factory.makeProduct(name='pr-a')
106
>>> task = factory.makeBugTask(target=prod_a)
107
>>> bug_summaries = store.find(
109
... BugSummary.product == prod_a,
110
... BugSummary.viewed_by == None,
111
... BugSummary.tag == None)
113
>>> print_result(bug_summaries)
114
--------------------------------------------------------
115
prod ps dist ds spn tag mile status vis #
116
--------------------------------------------------------
117
pr-a x x x x x x New x 1
121
There is one row per tag per combination of product, status and milestone.
122
If we are interested in all bugs targeted to a product regardless of how
123
they are tagged, we must specify BugSummary.tag == None. If we are
124
interested in all bugs targeted to a product regardless of their status
125
or milestone, we need to aggregate them.
127
>>> bug = factory.makeBug(product=prod_a, status=BugTaskStatus.NEW)
128
>>> bug = factory.makeBug(product=prod_a, status=BugTaskStatus.CONFIRMED)
129
>>> bug = factory.makeBug(
130
... product=prod_a, status=BugTaskStatus.CONFIRMED, tags=['t-a'])
133
Here are the untagged rows. This will show us there are 2 New and 2
134
Confirmed bug tasks targetted to the pr-a product.:
137
... BugSummary.product == prod_a,
138
... BugSummary.tag == None,
139
... BugSummary.viewed_by == None)
140
--------------------------------------------------------
141
prod ps dist ds spn tag mile status vis #
142
--------------------------------------------------------
143
pr-a x x x x x x New x 2
144
pr-a x x x x x x Confirmed x 2
148
Here are the rows associated with the 't-a' tag. There is 1 Confirmed
149
bug task targetted to the pr-a product who's bug is tagged 't-a'.:
152
... BugSummary.product == prod_a,
153
... BugSummary.tag == u't-a',
154
... BugSummary.viewed_by == None)
155
--------------------------------------------------------
156
prod ps dist ds spn tag mile status vis #
157
--------------------------------------------------------
158
pr-a x x x x t-a x Confirmed x 1
162
You will normally want to get the total count counted in the database
163
rather than waste transmission time to calculate the rows client side.
164
Note that sum() will return None if there are no matching rows, so we
165
need to cope with that:
169
... BugSummary.product == prod_a,
170
... BugSummary.viewed_by == None,
171
... BugSummary.tag == None).sum(BugSummary.count) or 0
176
... BugSummary.product == prod_a,
177
... BugSummary.viewed_by == None,
178
... BugSummary.tag == u't-a').sum(BugSummary.count) or 0
181
If you neglect to specify the tag clause, you will get an incorrect
182
total (so far, we have created only 4 bugs):
186
... BugSummary.viewed_by == None,
187
... BugSummary.product==prod_a).sum(BugSummary.count) or 0
190
Milestones works similarly, except you leave out the milestone clause
191
to calculate totals regardless of milestone. If you explicitly query for
192
the NULL milestone, you are retrieving information on bugs that have not
193
been assigned to a milestone:
195
>>> milestone = factory.makeMilestone(product=prod_a, name='ms-a')
196
>>> bug = factory.makeBug(milestone=milestone, tags=['t-b', 't-c'])
198
... BugSummary.product == prod_a,
199
... BugSummary.viewed_by == None)
200
--------------------------------------------------------
201
prod ps dist ds spn tag mile status vis #
202
--------------------------------------------------------
203
pr-a x x x x t-a x Confirmed x 1
204
pr-a x x x x t-b ms-a New x 1
205
pr-a x x x x t-c ms-a New x 1
206
pr-a x x x x x ms-a New x 1
207
pr-a x x x x x x New x 2
208
pr-a x x x x x x Confirmed x 2
212
Number of New bugs not targeted to a milestone. Note the difference
213
between selecting records where tag is None, and where milestone is None:
217
... BugSummary.product == prod_a,
218
... BugSummary.status == BugTaskStatus.NEW,
219
... BugSummary.viewed_by == None,
220
... BugSummary.milestone == None,
221
... BugSummary.tag == None).sum(BugSummary.count) or 0
224
Number of bugs targeted to prod_a, grouped by milestone:
226
>>> from lp.registry.model.milestone import Milestone
227
>>> from storm.expr import Sum, LeftJoin
229
... BugSummary, Milestone, BugSummary.milestone_id == Milestone.id)
230
>>> results = store.using(join).find(
231
... (Milestone, Sum(BugSummary.count)),
232
... BugSummary.product == prod_a,
233
... BugSummary.viewed_by == None,
234
... BugSummary.tag == None
235
... ).group_by(Milestone).order_by(Milestone.name)
236
>>> for milestone, count in results:
238
... print milestone.name, count
240
... print None, count
245
ProductSeries Bug Counts
246
------------------------
248
Querying for ProductSeries information is identical to querying for
249
Product information except you patch on the productseries column instead
250
of the product column. Note that if there is a BugTask targetting a
251
ProductSeries, there also must be a BugTask record targetting that
252
ProductSeries' Product:
254
>>> from lp.registry.model.productseries import ProductSeries
255
>>> prod_b = factory.makeProduct(name='pr-b')
256
>>> productseries_b = factory.makeProductSeries(
257
... product=prod_b, name='ps-b')
258
>>> bug_task = factory.makeBugTask(target=productseries_b)
259
>>> from storm.expr import Or
262
... BugSummary.productseries == productseries_b,
263
... BugSummary.product == prod_b),
264
... BugSummary.viewed_by == None)
265
--------------------------------------------------------
266
prod ps dist ds spn tag mile status vis #
267
--------------------------------------------------------
268
pr-b x x x x x x New x 1
269
x ps-b x x x x x New x 1
273
Distribution Bug Counts
274
-----------------------
276
Querying for Distribution bug count information is similar to querying
277
for Product information. Firstly, of course, you need to match on the
278
distribution column instead of the product column. The second difference
279
is you also have the sourcepackagename column to deal with, which acts
282
>>> distribution = factory.makeDistribution(name='di-a')
283
>>> package = factory.makeDistributionSourcePackage(
284
... distribution=distribution, sourcepackagename='sp-a')
286
>>> bug = factory.makeBug(
287
... distribution=distribution, status=BugTaskStatus.CONFIRMED)
288
>>> bug_task = factory.makeBugTask(target=package) # status is NEW
291
... BugSummary.distribution == distribution,
292
... BugSummary.viewed_by == None)
293
--------------------------------------------------------
294
prod ps dist ds spn tag mile status vis #
295
--------------------------------------------------------
296
x x di-a x sp-a x x New x 1
297
x x di-a x x x x New x 1
298
x x di-a x x x x Confirmed x 1
302
How many bugs targeted to a distribution?
306
... BugSummary.distribution == distribution,
307
... BugSummary.sourcepackagename == None,
308
... BugSummary.viewed_by == None,
309
... BugSummary.tag == None).sum(BugSummary.count) or 0
312
How many NEW bugs targeted to a distribution?
316
... BugSummary.distribution == distribution,
317
... BugSummary.sourcepackagename == None,
318
... BugSummary.status == BugTaskStatus.NEW,
319
... BugSummary.viewed_by == None,
320
... BugSummary.tag == None).sum(BugSummary.count) or 0
323
How many bugs targeted to a particular sourcepackage in a distribution?
327
... BugSummary.distribution == distribution,
328
... BugSummary.sourcepackagename == package.sourcepackagename,
329
... BugSummary.viewed_by == None,
330
... BugSummary.tag == None).sum(BugSummary.count) or 0
333
How many Confirmed bugs for a distribution have not been linked to a
334
sourcepackage? This is tricky, as we cannot directly ask for counts
335
not linked to a sourcepackage. We can however ask for counts linked to
336
a sourcepackage, so we subtract this count from the total number of bugs
337
targeted to the distribution:
339
>>> from storm.expr import SQL
342
... BugSummary.distribution == distribution,
343
... BugSummary.status == BugTaskStatus.CONFIRMED,
344
... BugSummary.viewed_by == None,
345
... BugSummary.tag == None).sum(SQL("""
346
... CASE WHEN sourcepackagename IS NULL THEN count ELSE -count END
351
DistroSeries Bug Counts
352
-----------------------
354
DistroSeries bug summary queries work the same as Distribution ones.
355
Just query using the distroseries column instead of the distribution
358
>>> distribution_c = factory.makeDistribution(name='di-c')
359
>>> series_c = factory.makeDistroRelease(
360
... distribution=distribution_c, name='ds-c')
361
>>> bug = factory.makeBugTask(target=series_c)
363
... BugSummary.distroseries == series_c,
364
... BugSummary.viewed_by == None)
365
--------------------------------------------------------
366
prod ps dist ds spn tag mile status vis #
367
--------------------------------------------------------
368
x x x ds-c x x x New x 1
376
So far, all the examples have been dealing with public bugs only.
377
They can all be extended to include private bug totals by using the
378
BugSummary.viewed_by column to join with the TeamParticipation table.
379
This join needs to be an OUTER JOIN.
381
>>> from lp.registry.model.teammembership import TeamParticipation
383
For our examples, first create three people. person_z will not
384
be subscribed to any bugs, so will have no access to any private bugs.
386
>>> person_a = factory.makePerson(name='p-a')
387
>>> person_b = factory.makePerson(name='p-b')
388
>>> person_z = factory.makePerson(name='p-z')
389
>>> owner = factory.makePerson(name='own')
391
Create some teams too. team_a just has person_a as a member. team_c
392
has both person_a and person_b as members. These teams will be subscribed
395
>>> team_a = factory.makeTeam(name='t-a', members=[person_a])
396
>>> team_c = factory.makeTeam(name='t-c', members=[person_a, person_b])
399
- bug_a is a private distribution bug, subscribed by team_a
400
- bug_b is a private distribution bug, subscribed by person_b
401
- bug_c is a private distroseries bug, which also gets an implicit
402
distribution task. Subscribed to by team_c.
405
>>> distro_p = factory.makeDistribution(name='di-p')
406
>>> series_p = factory.makeDistroRelease(
407
... distribution=distro_p, name='ds-p')
408
>>> bug_a = factory.makeBug(
409
... owner=owner, distribution=distro_p, private=True)
410
>>> bug_b = factory.makeBug(
411
... owner=owner, distribution=distro_p, private=True)
412
>>> bug_c = factory.makeBug(
413
... owner=owner, series=series_p, private=True)
414
>>> bug_z = factory.makeBug(
415
... owner=owner, distribution=distro_p, private=False)
417
>>> sub = bug_a.subscribe(team_a, person_a)
418
>>> sub = bug_b.subscribe(person_b, person_b)
419
>>> sub = bug_c.subscribe(team_c, person_a)
421
Whew! Check out what the BugSummary records now look like:
423
>>> distro_or_series = Or(
424
... BugSummary.distribution == distro_p,
425
... BugSummary.distroseries == series_p)
426
>>> print_find(distro_or_series)
427
--------------------------------------------------------
428
prod ps dist ds spn tag mile status vis #
429
--------------------------------------------------------
430
x x di-p x x x x New p-b 1
431
x x di-p x x x x New own 3
432
x x di-p x x x x New t-a 1
433
x x di-p x x x x New t-c 1
434
x x di-p x x x x New x 1
435
x x x ds-p x x x New own 1
436
x x x ds-p x x x New t-c 1
440
So how many public bugs are there on the distro?
444
... BugSummary.distribution == distro_p,
445
... BugSummary.viewed_by == None, # Public bugs only
446
... BugSummary.sourcepackagename == None,
447
... BugSummary.tag == None).sum(BugSummary.count) or 0
450
But how many can the owner see?
453
... BugSummary, TeamParticipation,
454
... BugSummary.viewed_by_id == TeamParticipation.teamID)
455
>>> store.using(join).find(
457
... BugSummary.distribution == distro_p,
459
... BugSummary.viewed_by == None,
460
... TeamParticipation.person == owner),
461
... BugSummary.sourcepackagename == None,
462
... BugSummary.tag == None).sum(BugSummary.count) or 0
465
Note that private bug counts can be inaccurate if a user is subscribed
466
through multiple paths, such as being directly subscribed and also
467
subscribed through a team. We demonstrate this using the distroseries
468
query - there is only a single private bug targeted to series_p.
470
>>> store.using(join).find(
472
... BugSummary.distroseries == series_p,
474
... BugSummary.viewed_by == None,
475
... TeamParticipation.person == person_a),
476
... BugSummary.sourcepackagename == None,
477
... BugSummary.tag == None).sum(BugSummary.count) or 0
480
>>> direct_sub = bug_c.subscribe(person_a, person_a)
482
>>> store.using(join).find(
484
... BugSummary.distroseries == series_p,
486
... BugSummary.viewed_by == None,
487
... TeamParticipation.person == person_a),
488
... BugSummary.sourcepackagename == None,
489
... BugSummary.tag == None).sum(BugSummary.count) or 0