~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-05-23 18:43:31 UTC
  • mfrom: (13084.2.6 page-match-rewrite-url)
  • Revision ID: launchpad@pqm.canonical.com-20110523184331-dhd2c7cgfuu49epw
[r=sinzui][bug=784273] Adds facility to the PageMatch to handle bad
        URIs

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
 
    ...
48
 
    ...     # And rollup the BugSummaryJournal into BugSummary
49
 
    ...     # so all the records are in one place.
50
 
    ...     store.execute("SELECT bugsummary_rollup_journal()")
51
 
    ...
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"
60
 
    ...     header = fmt % (
61
 
    ...         'prod', 'ps', 'dist', 'ds', 'spn', 'tag', 'mile',
62
 
    ...         'status', 'vis', '#')
63
 
    ...     print "-" * len(header)
64
 
    ...     print header
65
 
    ...     print "-" * len(header)
66
 
    ...     for bugsummary in ordered_results:
67
 
    ...         print fmt % (
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),
77
 
    ...             bugsummary.count)
78
 
    ...     print " " * (len(header) - 4),
79
 
    ...     print "==="
80
 
    ...     sum = bugsummary_resultset.sum(BugSummary.count)
81
 
    ...     print " " * (len(header) - 4),
82
 
    ...     print "%3s" % sum
83
 
 
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)
87
 
 
88
 
 
89
 
/!\ A Note About Privacy in These Examples 
90
 
------------------------------------------
91
 
 
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.
95
 
 
96
 
To count private bugs, these queries need to join with the
97
 
TeamParticipation table as detailed in the Privacy section.
98
 
 
99
 
 
100
 
Product Bug Counts
101
 
------------------
102
 
 
103
 
We can query for how many bugs are targeted to a product.
104
 
 
105
 
    >>> prod_a = factory.makeProduct(name='pr-a')
106
 
    >>> task = factory.makeBugTask(target=prod_a)
107
 
    >>> bug_summaries = store.find(
108
 
    ...     BugSummary,
109
 
    ...     BugSummary.product == prod_a,
110
 
    ...     BugSummary.viewed_by == None,
111
 
    ...     BugSummary.tag == None)
112
 
 
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
118
 
                                                         ===
119
 
                                                           1
120
 
 
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.
126
 
 
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'])
131
 
 
132
 
 
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.:
135
 
 
136
 
    >>> print_find(
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
145
 
                                                         ===
146
 
                                                           4
147
 
 
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'.:
150
 
 
151
 
    >>> print_find(
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
159
 
                                                         ===
160
 
                                                           1
161
 
 
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:
166
 
 
167
 
    >>> store.find(
168
 
    ...     BugSummary,
169
 
    ...     BugSummary.product == prod_a,
170
 
    ...     BugSummary.viewed_by == None,
171
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
172
 
    4
173
 
 
174
 
    >>> store.find(
175
 
    ...     BugSummary,
176
 
    ...     BugSummary.product == prod_a,
177
 
    ...     BugSummary.viewed_by == None,
178
 
    ...     BugSummary.tag == u't-a').sum(BugSummary.count) or 0
179
 
    1
180
 
 
181
 
If you neglect to specify the tag clause, you will get an incorrect
182
 
total (so far, we have created only 4 bugs):
183
 
 
184
 
    >>> store.find(
185
 
    ...     BugSummary,
186
 
    ...     BugSummary.viewed_by == None,
187
 
    ...     BugSummary.product==prod_a).sum(BugSummary.count) or 0
188
 
    5
189
 
 
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:
194
 
 
195
 
    >>> milestone = factory.makeMilestone(product=prod_a, name='ms-a')
196
 
    >>> bug = factory.makeBug(milestone=milestone, tags=['t-b', 't-c'])
197
 
    >>> print_find(
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
209
 
                                                         ===
210
 
                                                           8
211
 
 
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:
214
 
 
215
 
    >>> store.find(
216
 
    ...     BugSummary,
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
222
 
    2
223
 
 
224
 
Number of bugs targeted to prod_a, grouped by milestone:
225
 
 
226
 
    >>> from lp.registry.model.milestone import Milestone
227
 
    >>> from storm.expr import Sum, LeftJoin
228
 
    >>> join = 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:
237
 
    ...     if milestone:
238
 
    ...         print milestone.name, count
239
 
    ...     else:
240
 
    ...         print None, count
241
 
    ms-a 1
242
 
    None 4
243
 
 
244
 
 
245
 
ProductSeries Bug Counts
246
 
------------------------
247
 
 
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:
253
 
 
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
260
 
    >>> print_find(
261
 
    ...     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
270
 
                                                         ===
271
 
                                                           2
272
 
 
273
 
Distribution Bug Counts
274
 
-----------------------
275
 
 
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
280
 
the same as tag.
281
 
 
282
 
    >>> distribution = factory.makeDistribution(name='di-a')
283
 
    >>> package = factory.makeDistributionSourcePackage(
284
 
    ...     distribution=distribution, sourcepackagename='sp-a')
285
 
 
286
 
    >>> bug = factory.makeBug(
287
 
    ...     distribution=distribution, status=BugTaskStatus.CONFIRMED)
288
 
    >>> bug_task = factory.makeBugTask(target=package) # status is NEW
289
 
 
290
 
    >>> print_find(
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
299
 
                                                         ===
300
 
                                                           3
301
 
 
302
 
How many bugs targeted to a distribution?
303
 
 
304
 
    >>> store.find(
305
 
    ...     BugSummary,
306
 
    ...     BugSummary.distribution == distribution,
307
 
    ...     BugSummary.sourcepackagename == None,
308
 
    ...     BugSummary.viewed_by == None,
309
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
310
 
    2
311
 
 
312
 
How many NEW bugs targeted to a distribution?
313
 
 
314
 
    >>> store.find(
315
 
    ...     BugSummary,
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
321
 
    1
322
 
 
323
 
How many bugs targeted to a particular sourcepackage in a distribution?
324
 
 
325
 
    >>> store.find(
326
 
    ...     BugSummary,
327
 
    ...     BugSummary.distribution == distribution,
328
 
    ...     BugSummary.sourcepackagename == package.sourcepackagename,
329
 
    ...     BugSummary.viewed_by == None,
330
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
331
 
    1
332
 
 
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:
338
 
 
339
 
    >>> from storm.expr import SQL
340
 
    >>> store.find(
341
 
    ...     BugSummary,
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
347
 
    ...         """)) or 0
348
 
    1L
349
 
 
350
 
 
351
 
DistroSeries Bug Counts
352
 
-----------------------
353
 
 
354
 
DistroSeries bug summary queries work the same as Distribution ones.
355
 
Just query using the distroseries column instead of the distribution
356
 
column.
357
 
 
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)
362
 
    >>> print_find(
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
369
 
                                                         ===
370
 
                                                           1
371
 
 
372
 
 
373
 
Privacy
374
 
-------
375
 
 
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.
380
 
 
381
 
    >>> from lp.registry.model.teammembership import TeamParticipation
382
 
 
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.
385
 
 
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')
390
 
 
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
393
 
to private bugs.
394
 
 
395
 
    >>> team_a = factory.makeTeam(name='t-a', members=[person_a])
396
 
    >>> team_c = factory.makeTeam(name='t-c', members=[person_a, person_b])
397
 
 
398
 
Create some bugs.
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.
403
 
    - bug_z is public.
404
 
 
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)
416
 
 
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)
420
 
 
421
 
Whew! Check out what the BugSummary records now look like:
422
 
 
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
437
 
                                                         ===
438
 
                                                           9
439
 
 
440
 
So how many public bugs are there on the distro?
441
 
 
442
 
    >>> store.find(
443
 
    ...     BugSummary,
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
448
 
    1
449
 
 
450
 
But how many can the owner see?
451
 
 
452
 
    >>> join = LeftJoin(
453
 
    ...     BugSummary, TeamParticipation,
454
 
    ...     BugSummary.viewed_by_id == TeamParticipation.teamID)
455
 
    >>> store.using(join).find(
456
 
    ...     BugSummary,
457
 
    ...     BugSummary.distribution == distro_p,
458
 
    ...     Or(
459
 
    ...         BugSummary.viewed_by == None,
460
 
    ...         TeamParticipation.person == owner),
461
 
    ...     BugSummary.sourcepackagename == None,
462
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
463
 
    4
464
 
 
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.
469
 
 
470
 
    >>> store.using(join).find(
471
 
    ...     BugSummary,
472
 
    ...     BugSummary.distroseries == series_p,
473
 
    ...     Or(
474
 
    ...         BugSummary.viewed_by == None,
475
 
    ...         TeamParticipation.person == person_a),
476
 
    ...     BugSummary.sourcepackagename == None,
477
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
478
 
    1
479
 
 
480
 
    >>> direct_sub = bug_c.subscribe(person_a, person_a)
481
 
 
482
 
    >>> store.using(join).find(
483
 
    ...     BugSummary,
484
 
    ...     BugSummary.distroseries == series_p,
485
 
    ...     Or(
486
 
    ...         BugSummary.viewed_by == None,
487
 
    ...         TeamParticipation.person == person_a),
488
 
    ...     BugSummary.sourcepackagename == None,
489
 
    ...     BugSummary.tag == None).sum(BugSummary.count) or 0
490
 
    2