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()
47
... # Make sure our results are in a consistent order.
48
... ordered_results = bugsummary_resultset.order_by(
49
... BugSummary.product_id, BugSummary.productseries_id,
50
... BugSummary.distribution_id, BugSummary.distroseries_id,
51
... BugSummary.sourcepackagename_id, BugSummary.tag,
52
... BugSummary.milestone_id, BugSummary.status,
53
... BugSummary.viewed_by_id, BugSummary.id)
54
... fmt = "%-4s %-4s %-4s %-4s %-5s %-3s %-4s %-12s %-4s %3s"
56
... 'prod', 'ps', 'dist', 'ds', 'spn', 'tag', 'mile',
57
... 'status', 'vis', '#')
58
... print "-" * len(header)
60
... print "-" * len(header)
61
... for bugsummary in ordered_results:
63
... name(bugsummary.product),
64
... name(bugsummary.productseries),
65
... name(bugsummary.distribution),
66
... name(bugsummary.distroseries),
67
... name(bugsummary.sourcepackagename),
68
... bugsummary.tag or 'x',
69
... name(bugsummary.milestone),
70
... bugsummary.status,
71
... name(bugsummary.viewed_by),
73
... print " " * (len(header) - 4),
75
... sum = bugsummary_resultset.sum(BugSummary.count)
76
... print " " * (len(header) - 4),
79
>>> def print_find(*bs_query_args, **bs_query_kw):
80
... resultset = store.find(BugSummary, *bs_query_args, **bs_query_kw)
81
... print_result(resultset)
84
/!\ A Note About Privacy in These Examples
85
------------------------------------------
87
All the examples, except for the ones in the Privacy section, are
88
dealing with public bugs only. This is why they all are using
89
''BugSummary.viewed_by == None'' in their queries.
91
To count private bugs, these queries need to join with the
92
TeamParticipation table as detailed in the Privacy section.
98
We can query for how many bugs are targeted to a product.
100
>>> prod_a = factory.makeProduct(name='pr-a')
101
>>> task = factory.makeBugTask(target=prod_a)
102
>>> bug_summaries = store.find(
104
... BugSummary.product == prod_a,
105
... BugSummary.viewed_by == None,
106
... BugSummary.tag == None)
108
>>> print_result(bug_summaries)
109
--------------------------------------------------------
110
prod ps dist ds spn tag mile status vis #
111
--------------------------------------------------------
112
pr-a x x x x x x New x 1
116
There is one row per tag per combination of product, status and milestone.
117
If we are interested in all bugs targeted to a product regardless of how
118
they are tagged, we must specify BugSummary.tag == None. If we are
119
interested in all bugs targeted to a product regardless of their status
120
or milestone, we need to aggregate them.
122
>>> bug = factory.makeBug(product=prod_a, status=BugTaskStatus.NEW)
123
>>> bug = factory.makeBug(product=prod_a, status=BugTaskStatus.CONFIRMED)
124
>>> bug = factory.makeBug(
125
... product=prod_a, status=BugTaskStatus.CONFIRMED, tags=['t-a'])
128
Here are the untagged rows. This will show us there are 2 New and 2
129
Confirmed bug tasks targetted to the pr-a product.:
132
... BugSummary.product == prod_a,
133
... BugSummary.tag == None,
134
... BugSummary.viewed_by == None)
135
--------------------------------------------------------
136
prod ps dist ds spn tag mile status vis #
137
--------------------------------------------------------
138
pr-a x x x x x x New x 2
139
pr-a x x x x x x Confirmed x 2
143
Here are the rows associated with the 't-a' tag. There is 1 Confirmed
144
bug task targetted to the pr-a product who's bug is tagged 't-a'.:
147
... BugSummary.product == prod_a,
148
... BugSummary.tag == u't-a',
149
... BugSummary.viewed_by == None)
150
--------------------------------------------------------
151
prod ps dist ds spn tag mile status vis #
152
--------------------------------------------------------
153
pr-a x x x x t-a x Confirmed x 1
157
You will normally want to get the total count counted in the database
158
rather than waste transmission time to calculate the rows client side.
159
Note that sum() will return None if there are no matching rows, so we
160
need to cope with that:
164
... BugSummary.product == prod_a,
165
... BugSummary.viewed_by == None,
166
... BugSummary.tag == None).sum(BugSummary.count) or 0
171
... BugSummary.product == prod_a,
172
... BugSummary.viewed_by == None,
173
... BugSummary.tag == u't-a').sum(BugSummary.count) or 0
176
If you neglect to specify the tag clause, you will get an incorrect
177
total (so far, we have created only 4 bugs):
181
... BugSummary.viewed_by == None,
182
... BugSummary.product==prod_a).sum(BugSummary.count) or 0
185
Milestones works similarly, except you leave out the milestone clause
186
to calculate totals regardless of milestone. If you explicitly query for
187
the NULL milestone, you are retrieving information on bugs that have not
188
been assigned to a milestone:
190
>>> milestone = factory.makeMilestone(product=prod_a, name='ms-a')
191
>>> bug = factory.makeBug(milestone=milestone, tags=['t-b', 't-c'])
193
... BugSummary.product == prod_a,
194
... BugSummary.viewed_by == None)
195
--------------------------------------------------------
196
prod ps dist ds spn tag mile status vis #
197
--------------------------------------------------------
198
pr-a x x x x t-a x Confirmed x 1
199
pr-a x x x x t-b ms-a New x 1
200
pr-a x x x x t-c ms-a New x 1
201
pr-a x x x x x ms-a New x 1
202
pr-a x x x x x x New x 2
203
pr-a x x x x x x Confirmed x 2
207
Number of New bugs not targeted to a milestone. Note the difference
208
between selecting records where tag is None, and where milestone is None:
212
... BugSummary.product == prod_a,
213
... BugSummary.status == BugTaskStatus.NEW,
214
... BugSummary.viewed_by == None,
215
... BugSummary.milestone == None,
216
... BugSummary.tag == None).sum(BugSummary.count) or 0
219
Number of bugs targeted to prod_a, grouped by milestone:
221
>>> from lp.registry.model.milestone import Milestone
222
>>> from storm.expr import Sum, LeftJoin
224
... BugSummary, Milestone, BugSummary.milestone_id == Milestone.id)
225
>>> results = store.using(join).find(
226
... (Milestone, Sum(BugSummary.count)),
227
... BugSummary.product == prod_a,
228
... BugSummary.viewed_by == None,
229
... BugSummary.tag == None
230
... ).group_by(Milestone).order_by(Milestone.name)
231
>>> for milestone, count in results:
233
... print milestone.name, count
235
... print None, count
240
ProductSeries Bug Counts
241
------------------------
243
Querying for ProductSeries information is identical to querying for
244
Product information except you patch on the productseries column instead
245
of the product column. Note that if there is a BugTask targetting a
246
ProductSeries, there also must be a BugTask record targetting that
247
ProductSeries' Product:
249
>>> from lp.registry.model.productseries import ProductSeries
250
>>> prod_b = factory.makeProduct(name='pr-b')
251
>>> productseries_b = factory.makeProductSeries(
252
... product=prod_b, name='ps-b')
253
>>> bug_task = factory.makeBugTask(target=productseries_b)
254
>>> from storm.expr import Or
257
... BugSummary.productseries == productseries_b,
258
... BugSummary.product == prod_b),
259
... BugSummary.viewed_by == None)
260
--------------------------------------------------------
261
prod ps dist ds spn tag mile status vis #
262
--------------------------------------------------------
263
pr-b x x x x x x New x 1
264
x ps-b x x x x x New x 1
268
Distribution Bug Counts
269
-----------------------
271
Querying for Distribution bug count information is similar to querying
272
for Product information. Firstly, of course, you need to match on the
273
distribution column instead of the product column. The second difference
274
is you also have the sourcepackagename column to deal with, which acts
277
>>> distribution = factory.makeDistribution(name='di-a')
278
>>> package = factory.makeDistributionSourcePackage(
279
... distribution=distribution, sourcepackagename='sp-a')
281
>>> bug = factory.makeBug(
282
... distribution=distribution, status=BugTaskStatus.CONFIRMED)
283
>>> bug_task = factory.makeBugTask(target=package) # status is NEW
286
... BugSummary.distribution == distribution,
287
... BugSummary.viewed_by == None)
288
--------------------------------------------------------
289
prod ps dist ds spn tag mile status vis #
290
--------------------------------------------------------
291
x x di-a x sp-a x x New x 1
292
x x di-a x x x x New x 1
293
x x di-a x x x x Confirmed x 1
297
How many bugs targeted to a distribution?
301
... BugSummary.distribution == distribution,
302
... BugSummary.sourcepackagename == None,
303
... BugSummary.viewed_by == None,
304
... BugSummary.tag == None).sum(BugSummary.count) or 0
307
How many NEW bugs targeted to a distribution?
311
... BugSummary.distribution == distribution,
312
... BugSummary.sourcepackagename == None,
313
... BugSummary.status == BugTaskStatus.NEW,
314
... BugSummary.viewed_by == None,
315
... BugSummary.tag == None).sum(BugSummary.count) or 0
318
How many bugs targeted to a particular sourcepackage in a distribution?
322
... BugSummary.distribution == distribution,
323
... BugSummary.sourcepackagename == package.sourcepackagename,
324
... BugSummary.viewed_by == None,
325
... BugSummary.tag == None).sum(BugSummary.count) or 0
328
How many Confirmed bugs for a distribution have not been linked to a
329
sourcepackage? This is tricky, as we cannot directly ask for counts
330
not linked to a sourcepackage. We can however ask for counts linked to
331
a sourcepackage, so we subtract this count from the total number of bugs
332
targeted to the distribution:
334
>>> from storm.expr import SQL
337
... BugSummary.distribution == distribution,
338
... BugSummary.status == BugTaskStatus.CONFIRMED,
339
... BugSummary.viewed_by == None,
340
... BugSummary.tag == None).sum(SQL("""
341
... CASE WHEN sourcepackagename IS NULL THEN count ELSE -count END
346
DistroSeries Bug Counts
347
-----------------------
349
DistroSeries bug summary queries work the same as Distribution ones.
350
Just query using the distroseries column instead of the distribution
353
>>> distribution_c = factory.makeDistribution(name='di-c')
354
>>> series_c = factory.makeDistroRelease(
355
... distribution=distribution_c, name='ds-c')
356
>>> bug = factory.makeBugTask(target=series_c)
358
... BugSummary.distroseries == series_c,
359
... BugSummary.viewed_by == None)
360
--------------------------------------------------------
361
prod ps dist ds spn tag mile status vis #
362
--------------------------------------------------------
363
x x x ds-c x x x New x 1
371
So far, all the examples have been dealing with public bugs only.
372
They can all be extended to include private bug totals by using the
373
BugSummary.viewed_by column to join with the TeamParticipation table.
374
This join needs to be an OUTER JOIN.
376
>>> from lp.registry.model.teammembership import TeamParticipation
378
For our examples, first create three people. person_z will not
379
be subscribed to any bugs, so will have no access to any private bugs.
381
>>> person_a = factory.makePerson(name='p-a')
382
>>> person_b = factory.makePerson(name='p-b')
383
>>> person_z = factory.makePerson(name='p-z')
384
>>> owner = factory.makePerson(name='own')
386
Create some teams too. team_a just has person_a as a member. team_c
387
has both person_a and person_b as members. These teams will be subscribed
390
>>> team_a = factory.makeTeam(name='t-a', members=[person_a])
391
>>> team_c = factory.makeTeam(name='t-c', members=[person_a, person_b])
394
- bug_a is a private distribution bug, subscribed by team_a
395
- bug_b is a private distribution bug, subscribed by person_b
396
- bug_c is a private distroseries bug, which also gets an implicit
397
distribution task. Subscribed to by team_c.
400
>>> distro_p = factory.makeDistribution(name='di-p')
401
>>> series_p = factory.makeDistroRelease(
402
... distribution=distro_p, name='ds-p')
403
>>> bug_a = factory.makeBug(
404
... owner=owner, distribution=distro_p, private=True)
405
>>> bug_b = factory.makeBug(
406
... owner=owner, distribution=distro_p, private=True)
407
>>> bug_c = factory.makeBug(
408
... owner=owner, series=series_p, private=True)
409
>>> bug_z = factory.makeBug(
410
... owner=owner, distribution=distro_p, private=False)
412
>>> sub = bug_a.subscribe(team_a, person_a)
413
>>> sub = bug_b.subscribe(person_b, person_b)
414
>>> sub = bug_c.subscribe(team_c, person_a)
416
Whew! Check out what the BugSummary records now look like:
418
>>> distro_or_series = Or(
419
... BugSummary.distribution == distro_p,
420
... BugSummary.distroseries == series_p)
421
>>> print_find(distro_or_series)
422
--------------------------------------------------------
423
prod ps dist ds spn tag mile status vis #
424
--------------------------------------------------------
425
x x di-p x x x x New p-b 1
426
x x di-p x x x x New own 3
427
x x di-p x x x x New t-a 1
428
x x di-p x x x x New t-c 1
429
x x di-p x x x x New x 1
430
x x x ds-p x x x New own 1
431
x x x ds-p x x x New t-c 1
435
So how many public bugs are there on the distro?
439
... BugSummary.distribution == distro_p,
440
... BugSummary.viewed_by == None, # Public bugs only
441
... BugSummary.sourcepackagename == None,
442
... BugSummary.tag == None).sum(BugSummary.count) or 0
445
But how many can the owner see?
448
... BugSummary, TeamParticipation,
449
... BugSummary.viewed_by_id == TeamParticipation.teamID)
450
>>> store.using(join).find(
452
... BugSummary.distribution == distro_p,
454
... BugSummary.viewed_by == None,
455
... TeamParticipation.person == owner),
456
... BugSummary.sourcepackagename == None,
457
... BugSummary.tag == None).sum(BugSummary.count) or 0
460
Note that private bug counts can be inaccurate if a user is subscribed
461
through multiple paths, such as being directly subscribed and also
462
subscribed through a team. We demonstrate this using the distroseries
463
query - there is only a single private bug targeted to series_p.
465
>>> store.using(join).find(
467
... BugSummary.distroseries == series_p,
469
... BugSummary.viewed_by == None,
470
... TeamParticipation.person == person_a),
471
... BugSummary.sourcepackagename == None,
472
... BugSummary.tag == None).sum(BugSummary.count) or 0
475
>>> direct_sub = bug_c.subscribe(person_a, person_a)
477
>>> store.using(join).find(
479
... BugSummary.distroseries == series_p,
481
... BugSummary.viewed_by == None,
482
... TeamParticipation.person == person_a),
483
... BugSummary.sourcepackagename == None,
484
... BugSummary.tag == None).sum(BugSummary.count) or 0