~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/doc/bugsummary.txt

  • Committer: Julian Edwards
  • Date: 2011-06-06 11:49:08 UTC
  • mfrom: (7675.1045.467 db-devel)
  • mto: This revision was merged to the branch mainline in revision 13205.
  • Revision ID: julian.edwards@canonical.com-20110606114908-30jm0009t79ewsad
merge db-devel, there are some test failures as a result

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
Bug Count Summaries
 
2
===================
 
3
 
 
4
The BugSummary table contains summaries of bug counts. It contains one
 
5
row for every unique public BugTask targetting:
 
6
 
 
7
    - product
 
8
    - productseries
 
9
    - distribution
 
10
    - distroseries
 
11
    - sourcepackagename
 
12
    - tag
 
13
    - status
 
14
    - milestone
 
15
 
 
16
 
 
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.
 
19
 
 
20
    --------------------------------------------------------
 
21
    prod ps   dist ds   spn   tag mile status       vis    #
 
22
    --------------------------------------------------------
 
23
 
 
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.
 
27
 
 
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)
 
34
 
 
35
    >>> def name(object_or_none):
 
36
    ...     if object_or_none is None:
 
37
    ...         return 'x'
 
38
    ...     return object_or_none.name
 
39
 
 
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.
 
45
    ...     store.flush()
 
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"
 
55
    ...     header = fmt % (
 
56
    ...         'prod', 'ps', 'dist', 'ds', 'spn', 'tag', 'mile',
 
57
    ...         'status', 'vis', '#')
 
58
    ...     print "-" * len(header)
 
59
    ...     print header
 
60
    ...     print "-" * len(header)
 
61
    ...     for bugsummary in ordered_results:
 
62
    ...         print fmt % (
 
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),
 
72
    ...             bugsummary.count)
 
73
    ...     print " " * (len(header) - 4),
 
74
    ...     print "==="
 
75
    ...     sum = bugsummary_resultset.sum(BugSummary.count)
 
76
    ...     print " " * (len(header) - 4),
 
77
    ...     print "%3s" % sum
 
78
 
 
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)
 
82
 
 
83
 
 
84
/!\ A Note About Privacy in These Examples 
 
85
------------------------------------------
 
86
 
 
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.
 
90
 
 
91
To count private bugs, these queries need to join with the
 
92
TeamParticipation table as detailed in the Privacy section.
 
93
 
 
94
 
 
95
Product Bug Counts
 
96
------------------
 
97
 
 
98
We can query for how many bugs are targeted to a product.
 
99
 
 
100
    >>> prod_a = factory.makeProduct(name='pr-a')
 
101
    >>> task = factory.makeBugTask(target=prod_a)
 
102
    >>> bug_summaries = store.find(
 
103
    ...     BugSummary,
 
104
    ...     BugSummary.product == prod_a,
 
105
    ...     BugSummary.viewed_by == None,
 
106
    ...     BugSummary.tag == None)
 
107
 
 
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
 
113
                                                         ===
 
114
                                                           1
 
115
 
 
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.
 
121
 
 
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'])
 
126
 
 
127
 
 
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.:
 
130
 
 
131
    >>> print_find(
 
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
 
140
                                                         ===
 
141
                                                           4
 
142
 
 
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'.:
 
145
 
 
146
    >>> print_find(
 
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
 
154
                                                         ===
 
155
                                                           1
 
156
 
 
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:
 
161
 
 
162
    >>> store.find(
 
163
    ...     BugSummary,
 
164
    ...     BugSummary.product == prod_a,
 
165
    ...     BugSummary.viewed_by == None,
 
166
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
167
    4
 
168
 
 
169
    >>> store.find(
 
170
    ...     BugSummary,
 
171
    ...     BugSummary.product == prod_a,
 
172
    ...     BugSummary.viewed_by == None,
 
173
    ...     BugSummary.tag == u't-a').sum(BugSummary.count) or 0
 
174
    1
 
175
 
 
176
If you neglect to specify the tag clause, you will get an incorrect
 
177
total (so far, we have created only 4 bugs):
 
178
 
 
179
    >>> store.find(
 
180
    ...     BugSummary,
 
181
    ...     BugSummary.viewed_by == None,
 
182
    ...     BugSummary.product==prod_a).sum(BugSummary.count) or 0
 
183
    5
 
184
 
 
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:
 
189
 
 
190
    >>> milestone = factory.makeMilestone(product=prod_a, name='ms-a')
 
191
    >>> bug = factory.makeBug(milestone=milestone, tags=['t-b', 't-c'])
 
192
    >>> print_find(
 
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
 
204
                                                         ===
 
205
                                                           8
 
206
 
 
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:
 
209
 
 
210
    >>> store.find(
 
211
    ...     BugSummary,
 
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
 
217
    2
 
218
 
 
219
Number of bugs targeted to prod_a, grouped by milestone:
 
220
 
 
221
    >>> from lp.registry.model.milestone import Milestone
 
222
    >>> from storm.expr import Sum, LeftJoin
 
223
    >>> join = 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:
 
232
    ...     if milestone:
 
233
    ...         print milestone.name, count
 
234
    ...     else:
 
235
    ...         print None, count
 
236
    ms-a 1
 
237
    None 4
 
238
 
 
239
 
 
240
ProductSeries Bug Counts
 
241
------------------------
 
242
 
 
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:
 
248
 
 
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
 
255
    >>> print_find(
 
256
    ...     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
 
265
                                                         ===
 
266
                                                           2
 
267
 
 
268
Distribution Bug Counts
 
269
-----------------------
 
270
 
 
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
 
275
the same as tag.
 
276
 
 
277
    >>> distribution = factory.makeDistribution(name='di-a')
 
278
    >>> package = factory.makeDistributionSourcePackage(
 
279
    ...     distribution=distribution, sourcepackagename='sp-a')
 
280
 
 
281
    >>> bug = factory.makeBug(
 
282
    ...     distribution=distribution, status=BugTaskStatus.CONFIRMED)
 
283
    >>> bug_task = factory.makeBugTask(target=package) # status is NEW
 
284
 
 
285
    >>> print_find(
 
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
 
294
                                                         ===
 
295
                                                           3
 
296
 
 
297
How many bugs targeted to a distribution?
 
298
 
 
299
    >>> store.find(
 
300
    ...     BugSummary,
 
301
    ...     BugSummary.distribution == distribution,
 
302
    ...     BugSummary.sourcepackagename == None,
 
303
    ...     BugSummary.viewed_by == None,
 
304
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
305
    2
 
306
 
 
307
How many NEW bugs targeted to a distribution?
 
308
 
 
309
    >>> store.find(
 
310
    ...     BugSummary,
 
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
 
316
    1
 
317
 
 
318
How many bugs targeted to a particular sourcepackage in a distribution?
 
319
 
 
320
    >>> store.find(
 
321
    ...     BugSummary,
 
322
    ...     BugSummary.distribution == distribution,
 
323
    ...     BugSummary.sourcepackagename == package.sourcepackagename,
 
324
    ...     BugSummary.viewed_by == None,
 
325
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
326
    1
 
327
 
 
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:
 
333
 
 
334
    >>> from storm.expr import SQL
 
335
    >>> store.find(
 
336
    ...     BugSummary,
 
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
 
342
    ...         """)) or 0
 
343
    1L
 
344
 
 
345
 
 
346
DistroSeries Bug Counts
 
347
-----------------------
 
348
 
 
349
DistroSeries bug summary queries work the same as Distribution ones.
 
350
Just query using the distroseries column instead of the distribution
 
351
column.
 
352
 
 
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)
 
357
    >>> print_find(
 
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
 
364
                                                         ===
 
365
                                                           1
 
366
 
 
367
 
 
368
Privacy
 
369
-------
 
370
 
 
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.
 
375
 
 
376
    >>> from lp.registry.model.teammembership import TeamParticipation
 
377
 
 
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.
 
380
 
 
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')
 
385
 
 
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
 
388
to private bugs.
 
389
 
 
390
    >>> team_a = factory.makeTeam(name='t-a', members=[person_a])
 
391
    >>> team_c = factory.makeTeam(name='t-c', members=[person_a, person_b])
 
392
 
 
393
Create some bugs.
 
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.
 
398
    - bug_z is public.
 
399
 
 
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)
 
411
 
 
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)
 
415
 
 
416
Whew! Check out what the BugSummary records now look like:
 
417
 
 
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
 
432
                                                         ===
 
433
                                                           9
 
434
 
 
435
So how many public bugs are there on the distro?
 
436
 
 
437
    >>> store.find(
 
438
    ...     BugSummary,
 
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
 
443
    1
 
444
 
 
445
But how many can the owner see?
 
446
 
 
447
    >>> join = LeftJoin(
 
448
    ...     BugSummary, TeamParticipation,
 
449
    ...     BugSummary.viewed_by_id == TeamParticipation.teamID)
 
450
    >>> store.using(join).find(
 
451
    ...     BugSummary,
 
452
    ...     BugSummary.distribution == distro_p,
 
453
    ...     Or(
 
454
    ...         BugSummary.viewed_by == None,
 
455
    ...         TeamParticipation.person == owner),
 
456
    ...     BugSummary.sourcepackagename == None,
 
457
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
458
    4
 
459
 
 
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.
 
464
 
 
465
    >>> store.using(join).find(
 
466
    ...     BugSummary,
 
467
    ...     BugSummary.distroseries == series_p,
 
468
    ...     Or(
 
469
    ...         BugSummary.viewed_by == None,
 
470
    ...         TeamParticipation.person == person_a),
 
471
    ...     BugSummary.sourcepackagename == None,
 
472
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
473
    1
 
474
 
 
475
    >>> direct_sub = bug_c.subscribe(person_a, person_a)
 
476
 
 
477
    >>> store.using(join).find(
 
478
    ...     BugSummary,
 
479
    ...     BugSummary.distroseries == series_p,
 
480
    ...     Or(
 
481
    ...         BugSummary.viewed_by == None,
 
482
    ...         TeamParticipation.person == person_a),
 
483
    ...     BugSummary.sourcepackagename == None,
 
484
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
 
485
    2