~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
Milestones
==========


Utility function(s)
-------------------

We have a page which lists all the milestones for an object. This
function will print them out:

    >>> def all_milestones(browser):
    ...     table = find_main_content(browser.contents).find('tbody')
    ...     if table is None:
    ...         return None
    ...     result = []
    ...     for tr in table.findAll('tr'):
    ...         milestone_date = tr.find('span')
    ...         if len(milestone_date.contents) > 0:
    ...             # Just make sure we don't print an actual date.
    ...             milestone_date.contents[0].replaceWith('A date')
    ...         result.append(extract_text(tr))
    ...     return '\n'.join(result)

    >>> def milestones_in_portlet(browser):
    ...     portlet = find_portlet(browser.contents, 'Active milestones')
    ...     if portlet is None:
    ...         return None
    ...     result = []
    ...     for tr in portlet.find('table').findAll('tr'):
    ...         result.append(
    ...             ' '.join(text.strip() for text in tr.findAll(text=True)))
    ...     return '\n'.join(result)


Milestone listings
------------------

Products, distributions, product series, distribution series and
projects have a page in which all of their milestones are listed.


Distributions
.............

    >>> anon_browser.open('http://launchpad.dev/debian/+milestones')
    >>> anon_browser.url
    'http://launchpad.dev/debian/+milestones'

    >>> print all_milestones(anon_browser)
    Debian 3.1      Woody ...
    Debian 3.1-rc1  Woody ...


Distribution Series
...................

    >>> anon_browser.open('http://launchpad.dev/debian/woody/+milestones')
    >>> print all_milestones(anon_browser)
    Debian 3.1      ...
    Debian 3.1-rc1  ...

    >>> anon_browser.open('http://launchpad.dev/debian/sarge/+milestones')
    >>> print all_milestones(anon_browser)
    None


Products
........

The Product "All milestones" page lists the milestones for all series,
including the inactive ones. They do not include the bug and blueprint
counts (because they are costly to retrieve).

    >>> anon_browser.open('http://launchpad.dev/firefox/+milestones')
    >>> anon_browser.url
    'http://launchpad.dev/firefox/+milestones'

    >>> print all_milestones(anon_browser)
    Mozilla Firefox 1.0.0 "First Stable Release"    1.0    None        ...
    Mozilla Firefox 0.9.2 "One (secure) Tree Hill"  trunk  None        ...
    Mozilla Firefox 0.9.1 "One Tree Hill (v2)"      trunk  None        ...
    Mozilla Firefox 0.9   "One Tree Hill"           trunk  None        ...
    Mozilla Firefox 1.0                             trunk  A date
                                                              not yet released

When the project is a member of a project group, the the user can see a
link to the project groups's milestone's page.

    >>> anon_browser.getLink(
    ...     'View milestones for The Mozilla Project').click()
    >>> print anon_browser.title
    Milestones : The Mozilla Project


Product Series
..............

    >>> anon_browser.open('http://launchpad.dev/firefox/trunk/+milestones')
    >>> print all_milestones(anon_browser)
    Mozilla Firefox 0.9.2  ...
    Mozilla Firefox 0.9.1  ...
    Mozilla Firefox 0.9    ...
    Mozilla Firefox 1.0    ...

    >>> anon_browser.open('http://launchpad.dev/firefox/1.0/+milestones')
    >>> print all_milestones(anon_browser)
    Mozilla Firefox 1.0.0 ...


Project groups
..............

The project group "All milestones" page lists all milestones for all
products and series, including the inactive ones. They do not include
the bug and blueprint counts (because they are costly to retrieve).

    >>> from lp.testing import login, logout
    >>> from lp.registry.tests.test_project_milestone import (
    ...     ProjectMilestoneTest)
    >>> login('foo.bar@canonical.com')
    >>> test_helper = ProjectMilestoneTest(helper_only=True)
    >>> test_helper.setUpProjectMilestoneTests()
    >>> logout()
    >>> anon_browser.open('http://launchpad.dev/gnome')
    >>> anon_browser.getLink('See all milestones').click()
    >>> print all_milestones(anon_browser)
    GNOME 2.1.6  None        This is an inactive milestone
    GNOME 1.0    None        This is an inactive milestone
    GNOME 1.3    A date      This is an inactive milestone
    GNOME 1.2    A date      not yet released
    GNOME 1.1.   A date      not yet released
    GNOME 1.1    A date      not yet released


Individual milestones
---------------------

Pages for the individual milestones show all specifications and bugtasks
associated with that milestone for products of this project:

    >>> anon_browser.getLink('1.1', index=1).click()
    >>> print anon_browser.title
    1.1 : GNOME

    >>> specs = find_tag_by_id(anon_browser.contents, 'milestone_specs')
    >>> print extract_text(specs)
    Blueprint Project Priority Assignee Delivery
    Title evolution specification   Evolution  High  Unknown
    Title gnomebaker specification  gnomebaker High  Unknown

    >>> bugtasks = find_tag_by_id(anon_browser.contents, 'milestone_bugtasks')
    >>> print extract_text(bugtasks)
    Bug report Project Importance Assignee Status ...
    Milestone test bug for evolution  Evolution  Undecided Confirmed ...
    Milestone test bug for gnomebaker gnomebaker Undecided Confirmed ...
    Milestone test bug for evolution series trunk Undecided Confirmed

A project milestone page has the same navigation as the project:

    >>> anon_browser.open('http://launchpad.dev/firefox/+milestone/1.0')
    >>> print anon_browser.title
    1.0 : Mozilla Firefox

    >>> print_location(anon_browser.contents)
    Hierarchy: Mozilla Firefox > 1.0
    Tabs:
    * Overview (selected) - http://launchpad.dev/firefox
    * Code - http://code.launchpad.dev/firefox
    * Bugs - http://bugs.launchpad.dev/firefox
    * Blueprints - http://blueprints.launchpad.dev/firefox
    * Translations - http://translations.launchpad.dev/firefox
    * Answers - http://answers.launchpad.dev/firefox
    Main heading: Mozilla Firefox 1.0

Similarly, a distribution milestone page has the same navigation as the
distribution:

    >>> anon_browser.open('http://launchpad.dev/debian/+milestone/3.1')
    >>> print anon_browser.title
    3.1 : Debian

    >>> print_location(anon_browser.contents)
    Hierarchy: Debian > 3.1
    Tabs:
    * Overview (selected) - http://launchpad.dev/debian
    * Code - http://code.launchpad.dev/debian
    * Bugs - http://bugs.launchpad.dev/debian
    * Blueprints - http://blueprints.launchpad.dev/debian
    * Translations - http://translations.launchpad.dev/debian
    * Answers - http://answers.launchpad.dev/debian
    Main heading: Debian 3.1


Duplicate bugs
..............

Milestone bug listings do not show bugs that are marked as duplicates of
other bugs.

To demonstrate this, we'll begin by filing a couple of bugs for the
Mozilla Firefox product:

    >>> browser = setupBrowser(auth='Basic test@canonical.com:test')
    >>> browser.open('http://bugs.launchpad.dev/firefox/')
    >>> browser.getLink('Report a bug').click()
    >>> browser.getControl('Summary', index=0).value = 'Test Bug 1'
    >>> browser.getControl('Continue').click()

    >>> report_bug_url = browser.url

    >>> browser.getControl('Further information').value = 'Test Bug 1'
    >>> browser.getControl('Submit').click()
    >>> for message in get_feedback_messages(browser.contents):
    ...     print extract_text(message)
    Thank you for your bug report...

    >>> bug_1_url = browser.url
    >>> bug_1_id = bug_1_url.split('/')[-1]

    >>> browser.open(report_bug_url)
    >>> browser.getControl('Summary', index=0).value = 'Test Bug 2'
    >>> browser.getControl('Continue').click()

    >>> browser.getControl('Further information').value = 'Test Bug 2'
    >>> browser.getControl('Submit').click()
    >>> for message in get_feedback_messages(browser.contents):
    ...     print extract_text(message)
    Thank you for your bug report...

    >>> bug_2_url = browser.url
    >>> bug_2_id = bug_2_url.split('/')[-1]

Next, we'll target each bug to the 1.0 milestone:

    >>> browser.open(bug_1_url)
    >>> browser.getLink(url=bug_1_url + '/+editstatus').click()
    >>> control = browser.getControl('Milestone')
    >>> milestone_name = '1.0'
    >>> [milestone_id] = [
    ...     option.name for option in control.mech_control.items
    ...     if option.get_labels()[0].text.endswith(milestone_name)]
    >>> control.value = [milestone_id]
    >>> browser.getControl('Save Changes').click()

    >>> browser.open(bug_2_url)
    >>> browser.getLink(url=bug_2_url + '/+editstatus').click()
    >>> browser.getControl('Milestone').value = [milestone_id]
    >>> browser.getControl('Save Changes').click()

Observe that both bugs are listed in the 1.0 milestone listing:

    >>> browser.open('http://launchpad.dev/firefox/trunk')
    >>> browser.getLink('View milestones').click()
    >>> browser.getLink("Mozilla Firefox 1.0", index=1).click()
    >>> print browser.title
    1.0 : Mozilla Firefox

    >>> milestone_url = browser.url

    >>> browser.getLink('Test Bug 1').click()
    >>> browser.url == bug_1_url
    True

    >>> browser.open(milestone_url)
    >>> browser.getLink('Test Bug 2').click()
    >>> browser.url == bug_2_url
    True

Now we'll mark the second bug as a duplicate of the first:

    >>> browser.open(bug_2_url)
    >>> browser.getLink('Mark as duplicate').click()
    >>> browser.getControl('Duplicate').value = bug_1_id
    >>> browser.getControl('Change').click()
    >>> print extract_text(browser.contents)
    Bug #20 in Mozilla Firefox...
    Duplicate of bug #19...

Since duplicate bugs are not listed in milestone listings, only our
first bug is listed in the 1.0 milestone listing:

    >>> from lp.testing.layers import MemcachedLayer

    >>> MemcachedLayer.purge()

    >>> browser.open(milestone_url)
    >>> browser.getLink('Test Bug 1').click()
    >>> browser.url == bug_1_url
    True

    >>> browser.open(milestone_url)
    >>> print browser.getLink('Test Bug 2')
    Traceback (most recent call last):
        ...
    LinkNotFoundError

However, it's also possible to clear the duplicate status of our second
bug:

    >>> browser.open(bug_2_url)
    >>> browser.getLink(id='change_duplicate_bug').click()
    >>> browser.getControl('Duplicate').value = ''
    >>> browser.getControl('Change').click()
    >>> 'This report is a duplicate' in find_main_content(browser.contents)
    False

Now both bugs are listed in the 1.0 milestone listing once again:

    >>> MemcachedLayer.purge()

    >>> browser.open(milestone_url)
    >>> browser.getLink('Test Bug 1').click()
    >>> browser.url == bug_1_url
    True

    >>> browser.open(milestone_url)
    >>> browser.getLink('Test Bug 2').click()
    >>> browser.url == bug_2_url
    True


Bugs targeted to multiple series
................................

Bugs targeted to the same milestone across more than one series will
result in duplicate entries in the milestone listing (one for each
series target).

To demonstrate this, we'll begin by creating a new series "2.0" for the
Mozilla Firefox product, to complement the existing series "1.0":

    >>> browser.open('http://launchpad.dev/firefox')
    >>> browser.getLink('Register a series').click()
    >>> print browser.title
    Register a new Mozilla Firefox release series...

    >>> browser.getControl('Name').value = '2.0'
    >>> browser.getControl('Summary').value = 'The Firefox 2.0 Series'
    >>> browser.getControl('Register Series').click()
    >>> print browser.title
    Series 2.0 : Mozilla Firefox

We'll also create a new test milestone within the "trunk" series:

    >>> browser.open('http://launchpad.dev/firefox/trunk')
    >>> print browser.title
    Series trunk : Mozilla Firefox

    >>> browser.getLink('Create milestone').click()
    >>> print browser.title
    Register a new milestone...

    >>> milestone = 'test-milestone'
    >>> browser.getControl('Name').value = milestone
    >>> browser.getControl('Date Targeted').value = '2100-08-08'
    >>> browser.getControl('Register Milestone').click()
    >>> print browser.title
    Series trunk : Mozilla Firefox

    >>> MemcachedLayer.purge()
    >>> browser.open('http://launchpad.dev/firefox/trunk')

    >>> print extract_text(find_tag_by_id(browser.contents, 'series-trunk'))
    Version                         Expected    Released              Summary
    Mozilla Firefox 0.9.2...        Set date    Change details 2004-10-16  ...
    Mozilla Firefox...              Set date    Change details 2004-10-16  ...
    Mozilla Firefox test-milestone  2100-08-08  Release now ...

    >>> browser.getLink('test-milestone').click()
    >>> print browser.title
    test-milestone : Mozilla Firefox

    >>> milestone_url = browser.url

Let's target an existing bug to both series "1.0" and series "2.0":

    >>> from lp.services.helpers import backslashreplace
    >>> browser.open(bug_1_url)
    >>> print backslashreplace(browser.title)
    Bug...in Mozilla Firefox...

    >>> browser.getLink('Target to series').click()
    >>> print browser.title
    Target bug #19 to series...

    >>> browser.getControl('1.0').selected = True
    >>> browser.getControl('2.0').selected = True
    >>> browser.getControl('Target').click()

The bug page now lists a bug task for each series:

    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
    Affects Status Importance ...
    1.0     New    Undecided  ...
    2.0     New    Undecided  ...

Now we'll add each bug task to the same test milestone. Each bug task
has a link to an "edit status" form that can be used to choose the
milestone we're interested in. However, we need to be careful when
matching these links, as they may contain the same text as other links.
We'll use a specific URL pattern to avoid matching unrelated links.

Let's start with the first bug task:

    >>> import re
    >>> edit_status_url = re.compile(r'1.0/\+bug/[0-9]+/\+editstatus')
    >>> browser.getLink(url=edit_status_url).click()

Completing the "edit status" form allows us to add the bug task to the
milestone:

    >>> browser.getControl('Milestone').displayValue = [milestone]
    >>> browser.getControl('Importance').value = ['Critical']
    >>> browser.getControl('Save Changes').click()

    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
    Affects Status Importance ...
    1.0     New    Critical   ...

Now we'll add the second bug task to the test milestone, using the same
method. However this time we'll use a different importance:

    >>> edit_status_url = re.compile(r'2.0/\+bug/[0-9]+/\+editstatus')
    >>> browser.getLink(url=edit_status_url).click()
    >>> browser.getControl('Milestone').displayValue = [milestone]
    >>> browser.getControl('Importance').value = ['High']
    >>> browser.getControl('Save Changes').click()

    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
    Affects Status Importance ...
    2.0     New    High       ...

Observe that both bug tasks are now listed in the test milestone
listing:

    >>> browser.open(milestone_url)
    >>> bug_table = find_tag_by_id(browser.contents, 'milestone_bugtasks')
    >>> print extract_text(bug_table )
    Bug report       Importance  Assignee  Status
    #19 Test Bug 1   Critical              New
    #19 Test Bug 1   High                  New

Each bugtask has one or more badges.

    >>> print bug_table.findAll('tr')[1]
    <tr>...Test Bug 1...<a...alt="milestone test-milestone"...
      class="sprite milestone">...


Bugs targeted to development focus series
.........................................

When a bug is raised for a product or distribution, it is implicitly
targeted to the development focus series for that product or
distribution ("trunk" by default).

Ordinarily, targeting a bug to a milestone causes the bug to appear in
that milestone's bug listing:

    >>> browser.open(bug_2_url)
    >>> browser.getLink(url=bug_2_url + '/+editstatus').click()
    >>> browser.getControl('Milestone').displayValue = [milestone]
    >>> browser.getControl('Save Changes').click()

    >>> MemcachedLayer.purge()
    >>> browser.open(milestone_url)
    >>> print extract_text(find_tag_by_id(browser.contents,
    ...                                   'milestone_bugtasks'))
    Bug report...
    Test Bug 2...

When we explicitly target the bug to the development focus series, the
bug still appears in the milestone's bug listing:

    >>> browser.open(bug_2_url)
    >>> browser.getLink('Target to series').click()
    >>> print browser.url
    http://bugs.launchpad.dev/firefox/+bug/.../+nominate

    >>> browser.getControl('Trunk').selected = True
    >>> browser.getControl('Target').click()
    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
    Affects             Status                  ...
    Mozilla Firefox ... Status tracked in Trunk ...

    >>> browser.open(milestone_url)
    >>> bugtasks = extract_text(find_tag_by_id(browser.contents,
    ...                                        'milestone_bugtasks'))
    >>> print bugtasks
    Bug report...
    Test Bug 2...

Moreover, the bug appears only once in the listing:

    >>> print bugtasks.count('Test Bug 2')
    1