~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
= ProductSeries =

A Launchpad Product models a single piece of software. However for
release management purposes, a Product often has to be split in several
discrete entities which must be considered separately for packaging,
translations, version control, etc. We call these entities
ProductSeries.

    >>> from zope.component import getUtility
    >>> from lp.services.webapp.testing import verifyObject
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> from lp.registry.interfaces.product import IProductSet
    >>> from lp.registry.interfaces.productseries import IProductSeries
    >>> from lp.translations.interfaces.hastranslationimports import (
    ...     IHasTranslationImports)
    >>> from lp.services.database.sqlbase import flush_database_updates

First, get a product that has some ProductSeries in the sample data.

    >>> productset = getUtility(IProductSet)
    >>> firefox = productset['firefox']

A ProductSeries can be retrieved using the associated product and the
series name.

    >>> trunk = firefox.getSeries('trunk')

Verify that the resulting object correctly implements the IProductSeries
interface.

    >>> verifyObject(IProductSeries, trunk)
    True
    >>> IProductSeries.providedBy(trunk)
    True

and IHasTranslationImports.

    >>> verifyObject(IHasTranslationImports, trunk)
    True
    >>> IHasTranslationImports.providedBy(trunk)
    True

And verify that it looks like the series we think it should be.

    >>> trunk.product == firefox
    True
    >>> print trunk.name
    trunk

It's also possible to ask a product for all its associated series.

    >>> onedotzero = firefox.getSeries('1.0')
    >>> list(firefox.series) == [onedotzero, trunk]
    True

A ProductSeries can also be fetched with the IProductSeriesSet utility.

    >>> from lp.registry.interfaces.productseries import IProductSeriesSet

    >>> firefox_1_0 = getUtility(IProductSeriesSet).get(2)
    >>> print firefox_1_0.product.name
    firefox
    >>> print firefox_1_0.name
    1.0

New ProductSeries are created using Product.newSeries(). Only the product
owner or driver can call Product.newSeries().

    >>> series_driver = factory.makePerson(name="driver")
    >>> summary = "Port of Firefox to the Emacs operating system."
    >>> emacs = firefox.newSeries(series_driver , 'emacs', summary)
    Traceback (most recent call last):
    ...
    Unauthorized: (..., 'newSeries', 'launchpad.Driver')

    >>> login_person(firefox.owner)
    >>> emacs_series = firefox.newSeries(
    ...     firefox.owner , 'emacs', summary,
    ...     releasefileglob='ftp://gnu.org/emacs*.gz')
    >>> print emacs_series.name
    emacs

    >>> print emacs_series.summary
    Port of Firefox to the Emacs operating system.

    >>> print emacs_series.releasefileglob
    ftp://gnu.org/emacs*.gz

When a driver creates a series, he is also the driver of the new series
to make him the release manager.

    >>> firefox.driver = series_driver
    >>> login_person(series_driver)
    >>> emacs2 = firefox.newSeries(series_driver , 'emacs2', summary)
    >>> print emacs2.driver.name
    driver

A newly created series is assumed to be in the development state.

    >>> login(ANONYMOUS)
    >>> print emacs_series.status.title
    Active Development

Let's check that the new series is properly associated to its product.

    >>> flush_database_updates()
    >>> firefox.getSeries('emacs') == emacs_series
    True


= Drivers and release managers =

A driver for an IProduct or IProjectGroup cannot modify a product series.

    >>> from lp.services.webapp.authorization import check_permission

    >>> login_person(series_driver)
    >>> print emacs_series.owner.name
    name12
    >>> print emacs_series.driver
    None
    >>> check_permission('launchpad.Edit', emacs_series)
    False

A person appointed to the series driver has the release manager role and can
edit a product series.

    >>> login_person(firefox.owner)
    >>> emacs_series.driver = series_driver
    >>> login_person(series_driver)
    >>> check_permission('launchpad.Edit', emacs_series)
    True

    >>> login(ANONYMOUS)


== ProductSeries releassefileglob ==

Each ProductSeries may have a releassefileglob that describes the location
of where release files are uploaded to. The product release finder process
uses the releassefileglob to locate and retrieve files. The files are stored
in the librarian. Each fill is associated with a release. If the series
does not have a release for version in the file name, the finder will create
it. The finder will also create the series milestone too if it does not
exist. The success of product release finder to retrieve files, and create
milestone and releases, is largely predicated on the quality of the
releassefileglob.

The field is constrained by the validate_release_glob() function. It verifies
that the url uses one of the supported schemes (ftp, http, http).

    >>> from lp.registry.interfaces.productseries import (
    ...     validate_release_glob)

    >>> validate_release_glob('ftp://ftp.gnu.org/gnu/emacs/emacs-21.*.gz')
    True
    >>> validate_release_glob('http://ftp.gnu.org/gnu/emacs/emacs-21.*.gz')
    True
    >>> validate_release_glob('https://ftp.gnu.org/gnu/emacs/emacs-21.*.gz')
    True

Invalid URLs and unsupported schemes raise a LaunchpadValidationError.

    >>> validate_release_glob('ftp.gnu.org/gnu/emacs/emacs-21.*.gz')
    Traceback (most recent call last):
     ...
    LaunchpadValidationError: ...

    >>> validate_release_glob('wais://ftp.gnu.org/gnu/emacs/emacs-21.*.gz')
    Traceback (most recent call last):
     ...
    LaunchpadValidationError: ...

The URL must contain a glob (*) or , and may contain more than one.

    >>> validate_release_glob('http://ftp.gnu.org/gnu/emacs/emacs-21.10.1.gz')
    Traceback (most recent call last):
     ...
    LaunchpadValidationError: ...

    >>> validate_release_glob('http://ftp.gnu.org/gnu/*/emacs-21.*.gz')
    True


== Specification Listings ==

We should be able to get lists of specifications in different states
related to a productseries.

Basically, we can filter by completeness, and by whether or not the spec
is informational.

    >>> onezero = firefox.getSeries("1.0")
    >>> from lp.blueprints.enums import SpecificationFilter

We will create two specs for onezero and use them to demonstrate the
filtering.

    >>> from lp.services.database.constants import UTC_NOW
    >>> carlos = getUtility(IPersonSet).getByName('carlos')
    >>> from lp.blueprints.model.specification import Specification
    >>> a = Specification(name='a', title='A', summary='AA', owner=carlos,
    ...                   product=firefox, productseries=onezero,
    ...                   specurl='http://wbc.com/two', goal_proposer=carlos,
    ...                   date_goal_proposed=UTC_NOW)
    >>> b = Specification(name='b', title='b', summary='bb', owner=carlos,
    ...                   product=firefox, productseries=onezero,
    ...                   specurl='http://fds.com/adsf', goal_proposer=carlos,
    ...                   date_goal_proposed=UTC_NOW)

Now, we will make one of them accepted, the other declined, and both of
them informational.

    >>> from lp.blueprints.enums import (
    ...     SpecificationDefinitionStatus,
    ...     SpecificationImplementationStatus,
    ...     )
    >>> a.definition_status = b.definition_status = SpecificationDefinitionStatus.APPROVED
    >>> a.implementation_status = SpecificationImplementationStatus.INFORMATIONAL
    >>> b.implementation_status = SpecificationImplementationStatus.INFORMATIONAL
    >>> a.acceptBy(a.owner)
    >>> shim = a.updateLifecycleStatus(a.owner)
    >>> b.declineBy(b.owner)
    >>> shim = b.updateLifecycleStatus(b.owner)

    >>> from lp.services.database.sqlbase import flush_database_updates
    >>> flush_database_updates()

If we ask for ALL specs we should see them both.

    >>> filter = [SpecificationFilter.ALL]
    >>> for s in onezero.specifications(filter=filter):
    ...     print s.name
    a
    b

With a productseries, we can ask for ACCEPTED, PROPOSED and DECLINED
specs:

    >>> filter=[SpecificationFilter.ACCEPTED]
    >>> for spec in onezero.specifications(filter=filter):
    ...     print spec.name, spec.goalstatus.title
    a Accepted

    >>> filter=[SpecificationFilter.PROPOSED]
    >>> onezero.specifications(filter=filter).count()
    0

    >>> filter=[SpecificationFilter.DECLINED]
    >>> onezero.specifications(filter=filter).count()
    1

We should see one informational spec if we ask just for that, the
accepted one.

    >>> filter = [SpecificationFilter.INFORMATIONAL]
    >>> for s in onezero.specifications(filter=filter):
    ...     print s.name
    a

If we specifically ask for declined informational, we will get that:

    >>> filter = [
    ...    SpecificationFilter.INFORMATIONAL, SpecificationFilter.DECLINED]
    >>> for s in onezero.specifications(filter=filter):
    ...     print s.name
    b

There are is one completed, accepted spec for 1.0:

    >>> filter = [SpecificationFilter.COMPLETE]
    >>> for spec in onezero.specifications(filter=filter):
    ...    print spec.name, spec.is_complete, spec.goalstatus.title
    a True Accepted

There is one completed, declined spec:

    >>> filter = [SpecificationFilter.COMPLETE, SpecificationFilter.DECLINED]
    >>> for spec in onezero.specifications(filter=filter):
    ...    print spec.name, spec.is_complete, spec.goalstatus.title
    b True Declined

Now lets make b incomplete, but accepted.

    >>> b.implementation_status = SpecificationImplementationStatus.BETA
    >>> b.definition_status = SpecificationDefinitionStatus.NEW
    >>> shim = b.acceptBy(b.owner)
    >>> shim = b.updateLifecycleStatus(b.owner)
    >>> flush_database_updates()

And if we ask just for specs, we get BOTH the incomplete and the
complete ones that have been accepted.

    >>> for spec in onezero.specifications():
    ...     print spec.name, spec.is_complete, spec.goalstatus.title
    a True Accepted
    b False Accepted

We can search for text in specifications (in this case there are no
matches):

    >>> print len(list(onezero.specifications(filter=['new'])))
    0


== Lifecycle Management ==

In the example above, we use the acceptBy and updateLifecycleStatus methods on
a specification. These help us keep the full record of who moved the spec
through each relevant stage of its existence.

    >>> b.goal_decider is None
    False
    >>> b.goal_decider.name
    u'carlos'
    >>> b.date_completed is None
    True

There's a method which will tell us if status changes we have just made will
change the overall state of the spec to "completed".

    >>> jdub = getUtility(IPersonSet).getByName('jdub')
    >>> b.definition_status = SpecificationDefinitionStatus.APPROVED
    >>> b.implementation_status = SpecificationImplementationStatus.INFORMATIONAL
    >>> print b.updateLifecycleStatus(jdub).title
    Complete
    >>> b.completer.name
    u'jdub'
    >>> b.date_completed is None
    False


== Drivers ==

Products, projects and product series have drivers, who are people that
have permission to approve bugs and features for specific releases. The
rules are that:

 1. a "driver" can be set on either ProjectGroup, Product or ProductSeries

 2. drivers are only actually relevant on a ProductSeries, because thats
    the granularity at which we track spec/bug targeting

 3. the important attribute is ".drivers" on a productseries, it is
    calculated based on the combination of owners and drivers in the
    series, product and project. It is a LIST of drivers, which might be
    empty, or have one, two or three people/teams in it.

 4. the list includes the explicitly set drivers from series, product
    and project

 5. if there are no explicitly set drivers, then:
      - if there is a project, then the list is the project.owner
      - if there is no project, then the list is the product.owner in
    other words, we use the "highest" owner as the fallback, which is
    either the product owner or the project owner if there is a project.

We test these rules below. We will create the project, product and
series directly so that we don't have to deal with security permissions
checks when setting and resetting the driver attributes.

    >>> from lp.services.database.sqlbase import flush_database_updates
    >>> login('foo.bar@canonical.com')
    >>> carlos = getUtility(IPersonSet).getByName('carlos')
    >>> mark = getUtility(IPersonSet).getByName('mark')
    >>> jblack = getUtility(IPersonSet).getByName('jblack')

    >>> project = factory.makeProject(name='testproj',
    ...     displayname='Test Project',
    ...     title='Test Project Title', homepageurl='http://foo.com/url',
    ...     summary='summary', description='description', owner=carlos)
    >>> product = factory.makeProduct(owner=mark, name='testprod',
    ...     displayname='Test Product', title='Test product title',
    ...     summary='summary', project=project)
    >>> series = factory.makeProductSeries(owner=jblack, name='1.0', product=product,
    ...     summary='Series summary')


First, lets see what we get for the series drivers before we have
anything actually set.

If there is a project on the product, we would expect the project owner:

    >>> print series.product.project.name
    testproj
    >>> for d in series.drivers:
    ...     print d.name
    carlos

If there is NO project on the product, then we expect the product owner:

    >>> product.project = None
    >>> for d in series.drivers:
    ...     print d.name
    mark

Now lets put the project back:

    >>> product.project = project.id
    >>> flush_database_updates()

Edgar and cprov will be the drivers.

    >>> cprov = getUtility(IPersonSet).getByName('cprov')
    >>> edgar = getUtility(IPersonSet).getByName('edgar')

Edgar becomes the driver of the project group and thus also drives the
series.

    >>> project.driver = edgar
    >>> for d in series.drivers:
    ...     print d.name
    edgar

In addition cprov is made driver of the series. Both are drivers now.

    >>> series.driver = cprov
    >>> for d in series.drivers:
    ...     print d.name
    cprov
    edgar

With just a driver on the series, the owner of the project group is reported
as driver, too.

    >>> project.driver = None
    >>> for d in series.drivers:
    ...     print d.name
    carlos
    cprov

Without a project group, the driver role falls back to the product owner.

    >>> product.project = None
    >>> for d in series.drivers:
    ...     print d.name
    cprov
    mark