Merge lp:~adiroiban/launchpad/bug-531261 into lp:launchpad

Proposed by Adi Roiban
Status: Merged
Approved by: Guilherme Salgado
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~adiroiban/launchpad/bug-531261
Merge into: lp:launchpad
Diff against target: 595 lines (+150/-152)
8 files modified
lib/lp/registry/interfaces/distroseries.py (+11/-39)
lib/lp/registry/interfaces/productseries.py (+6/-41)
lib/lp/registry/interfaces/series.py (+53/-1)
lib/lp/registry/model/distroseries.py (+4/-38)
lib/lp/registry/model/productseries.py (+7/-26)
lib/lp/registry/model/series.py (+53/-0)
lib/lp/registry/stories/webservice/xx-distroseries.txt (+1/-0)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+15/-7)
To merge this branch: bzr merge lp:~adiroiban/launchpad/bug-531261
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Review via email: mp+20748@code.launchpad.net

Commit message

Move ISeriesMixin to lp.registry.interfaces.series together with summary, bug_supervisor, security_contact and drivers attributes.

Description of the change

= Bug 531261 =

Right now ISeriesMixin is located in lp.registry.interfaces.distroseries and is only used by DistroSeries.

Since this Mixin should be used for DistroSeries and ProductSeries common attributes, we should move it to lp.registry.interfaces.series, and make ProductSeries use it.

We can also see what other attributes can be moved to ISeriesMixin

== Proposed fix ==

As a start, move summary, bug_supervisor, security_contact and drivers in the common interface and mixin.

The other related bugs can be found here:
https://bugs.edge.launchpad.net/launchpad-registry/+bugs?field.tag=iseries

== Pre-implementation notes ==
Curtis has added his comments in the report for bug 531261.

== Implementation details ==

I don't know how to fix pylint warning about lazr modules. Any hint is much appreciated.

== Tests ==
./bin/test -t series

== Demo and Q/A ==

Since this is only a cleanup, there is no new functionality to test.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/interfaces/distroseries.py

== Pylint notices ==

lib/lp/registry/interfaces/distroseries.py
    22: [F0401] Unable to import 'lazr.enum' (No module named enum)
    23: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    28: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    112: [E1002, DistroSeriesVersionField._validate] Use super on an old style class
    423: [C0322, IDistroSeriesPublic.getPackageUploads] Operator not preceded by a space
    description=_(
    ^
    "Return items that are more recent than this timestamp."),
    required=False),
    status=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Package Upload Status"),
    description=_("Return only items that have this status."),
    required=False),
    archive=Reference(

    schema=Interface,
    title=_("Archive"),
    description=_("Return only items for this archive."),
    required=False),
    pocket=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Pocket"),
    description=_("Return only items targeted to this pocket"),
    required=False),
    custom_type=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Custom Type"),
    description=_("Return only items with custom files of this "
    "type."),
    required=False),
    )

    @operation_returns_collection_of(Interface)
    @export_read_operation()
    def getPackageUploads(created_since_date, status, archive, pocket,
    custom_type):

To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (3.8 KiB)

Hi Adi,

This is a nice refactoring; thanks for doing it.

There's just one thing I noticed which was not mentioned in your cover
letter: the IProductSeries interface has grown a 'active' attribute,
thanks to ISeriesMixin. Since your branch doesn't add any code that
uses that attribute, I don't see a reason for adding it, so I'd suggest
moving it to IDistroSeries and the actual implementation to
DistroSeries. Unless you do plan to use IProductSeries.active
somewhere?

On Fri, 2010-03-05 at 13:51 +0000, Adi Roiban wrote:
> Adi Roiban has proposed merging lp:~adiroiban/launchpad/bug-531261
> into lp:launchpad/devel.
>
[...]
> == Implementation details ==
>
> I don't know how to fix pylint warning about lazr modules. Any hint is
> much appreciated.

These are spurious, but I don't know how to silence pylint,
unfortunately.

>
> == Tests ==
> ./bin/test -t series
>
> == Demo and Q/A ==
>
> Since this is only a cleanup, there is no new functionality to test.
>
> = Launchpad lint =
>
> Checking for conflicts. and issues in doctests and templates.
> Running jslint, xmllint, pyflakes, and pylint.
> Using normal rules.
>
> Linting changed files:
> lib/lp/registry/interfaces/distroseries.py
>
>
> == Pylint notices ==
>
> lib/lp/registry/interfaces/distroseries.py
> 22: [F0401] Unable to import 'lazr.enum' (No module named enum)
> 23: [F0401] Unable to import 'lazr.restful.declarations' (No
> module named restful)
> 28: [F0401] Unable to import 'lazr.restful.fields' (No module
> named restful)
> 112: [E1002, DistroSeriesVersionField._validate] Use super on an
> old style class

> === modified file 'lib/lp/registry/interfaces/series.py'
> --- lib/lp/registry/interfaces/series.py 2009-12-13 11:55:40 +0000
> +++ lib/lp/registry/interfaces/series.py 2010-03-05 13:51:28 +0000
> @@ -76,3 +88,45 @@
> haven't started working yet.
> """)
>
> +
> +class ISeriesMixin(IHasDrivers):
> + """Methods & properties shared between distro & product series."""
> +
> + active = exported(
> + Bool(

In this case, having the first argument on a line by itself doesn't buy
us much as it allows you to move only one char to the left, so better to
leave it on the same line as Bool.

> + title=_("Active"),
> + description=_(
> + "Whether or not this series is stable and supported, or "
> + "under current development. This excludes series which "
> + "are experimental or obsolete.")))
> +
> + summary = exported(
> + Summary(title=_("Summary"),
> + description=_('A single paragraph that explains the goals of '
> + 'of this series and the intended users. '
> + 'For example: "The 2.0 series of Apache '
> + 'represents the current stable series, '
> + 'and is recommended for all new deployments".'),
> + required=True))
> +
> + drivers = exported(
> + CollectionField(
> + title=_(
> + 'A list of the people or teams who are drivers for this '
> + 'series. This l...

Read more...

Revision history for this message
Adi Roiban (adiroiban) wrote :

> Hi Adi,
>
> This is a nice refactoring; thanks for doing it.
>
> There's just one thing I noticed which was not mentioned in your cover
> letter: the IProductSeries interface has grown a 'active' attribute,
> thanks to ISeriesMixin. Since your branch doesn't add any code that
> uses that attribute, I don't see a reason for adding it, so I'd suggest
> moving it to IDistroSeries and the actual implementation to
> DistroSeries. Unless you do plan to use IProductSeries.active
> somewhere?
>
In the current code from lp/devel 'active' is the only attribute of ISeriesMixin. I was thinking it was put there to be used in the future.

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (3.4 KiB)

Hi, as you suggested, I moved „active” from ISeries to IDistroSeries.

Here is the latest diff:
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2010-03-06 01:24:05 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2010-03-08 09:08:46 +0000
@@ -164,6 +164,12 @@
             description=_(
                 "The title of this series. It should be distinctive "
                 "and designed to look good at the top of a page.")))
+ active = exported(Bool(
+ title=_("Active"),
+ description=_(
+ "Whether or not this series is stable and supported, or "
+ "under current development. This excludes series which "
+ "are experimental or obsolete.")))
     description = exported(
         Description(title=_("Description"), required=True,
             description=_("A detailed description of this series, with "

=== modified file 'lib/lp/registry/interfaces/series.py'
--- lib/lp/registry/interfaces/series.py 2010-03-06 01:24:05 +0000
+++ lib/lp/registry/interfaces/series.py 2010-03-08 09:07:19 +0000
@@ -92,13 +92,6 @@
 class ISeriesMixin(IHasDrivers):
     """Methods & properties shared between distro & product series."""

- active = exported(Bool(
- title=_("Active"),
- description=_(
- "Whether or not this series is stable and supported, or "
- "under current development. This excludes series which "
- "are experimental or obsolete.")))
-
     summary = exported(
         Summary(title=_("Summary"),
              description=_('A single paragraph that explains the goals of '

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-03-05 15:50:47 +0000
+++ lib/lp/registry/model/distroseries.py 2010-03-08 09:09:20 +0000
@@ -183,6 +183,15 @@
         intermediateTable='SectionSelection')

     @property
+ def active(self):
+ return self.status in [
+ SeriesStatus.DEVELOPMENT,
+ SeriesStatus.FROZEN,
+ SeriesStatus.CURRENT,
+ SeriesStatus.SUPPORTED,
+ ]
+
+ @property
     def named_version(self):
         return '%s (%s)' % (self.displayname, self.version)

=== modified file 'lib/lp/registry/model/series.py'
--- lib/lp/registry/model/series.py 2010-03-05 13:14:49 +0000
+++ lib/lp/registry/model/series.py 2010-03-08 09:08:56 +0000
@@ -25,15 +25,6 @@
     summary = StringCol(notNull=True)

     @property
- def active(self):
- return self.status in [
- SeriesStatus.DEVELOPMENT,
- SeriesStatus.FROZEN,
- SeriesStatus.CURRENT,
- SeriesStatus.SUPPORTED,
- ]
-
- @property
     def bug_supervisor(self):
         """See `ISeriesMixin`."""
         return self.parent.bug_supervisor

=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-05 13:14:49 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-08 09:11:47 +0000
@@ -810,7 +810,6 @@

     >>> babadoo_foobadoo = webservice.get('/babadoo/foobadoo'...

Read more...

Revision history for this message
Adi Roiban (adiroiban) wrote :

This is a small diff that fixes a spurious test, as a few line below the test is using both display_name and name:

    >>> for project in project_entries[:5]:
    ... print "%s (%s)" % (project['display_name'], project['name'])
    APTonCD (aptoncd)
    Arch mirrors (arch-mirrors)
    Bazaar (bazaar)
    Bazaar (bzr)
    Derby (derby)

=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-08 09:34:22 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-08 10:07:31 +0000
@@ -500,7 +500,7 @@
     >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()

     >>> project_entries = sorted(
- ... project_collection['entries'], key=itemgetter('display_name'))
+ ... project_collection['entries'], key=itemgetter('display_name','name'))
     >>> len(project_entries)
     25

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Mon, 2010-03-08 at 10:13 +0000, Adi Roiban wrote:
> This is a small diff that fixes a spurious test, as a few line below the test is using both display_name and name:
>
> >>> for project in project_entries[:5]:
> ... print "%s (%s)" % (project['display_name'], project['name'])
> APTonCD (aptoncd)
> Arch mirrors (arch-mirrors)
> Bazaar (bazaar)
> Bazaar (bzr)
> Derby (derby)
>
> === modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
> --- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-08 09:34:22 +0000
> +++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-08 10:07:31 +0000
> @@ -500,7 +500,7 @@
> >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()
>
> >>> project_entries = sorted(
> - ... project_collection['entries'], key=itemgetter('display_name'))
> + ... project_collection['entries'], key=itemgetter('display_name','name'))

This line has more than 80 chars now, no? Can you break it between the
two args?

 review approve

--
Guilherme Salgado <email address hidden>

review: Approve
Revision history for this message
Adi Roiban (adiroiban) wrote :

I have fixed the long line + some other long lines in this file.

"./bin/test -m registry" has no errors on my system.

Do you have time to sent this branch for landing?

Many thanks!

Revision history for this message
Guilherme Salgado (salgado) wrote :

Sure; I've just submitted it.

Revision history for this message
Guilherme Salgado (salgado) wrote :

The test suite hanged on ec2, but there are a few failures: http://paste.ubuntu.com/391282/

Adi, can you run these tests locally and let me know if the pass or if you need to make any changes to have them passing again?

Revision history for this message
Adi Roiban (adiroiban) wrote :

Hi,

There are no errors running buildd-slavescanner.txt tests with this branch on my computer.

---------------

./bin/test -t test_programming_languages_edit -t test_project_licenses -t test_product_edit_people_owner -t test_product_edit_people_driver -t test_title_inline_edit

These Windmill tests are randomly failing.
They also fail on my computer for the lp/devel branch.

They are AJAX action to modify product properties.
I have watch them using „./bin/test -D -t test_programming_languages_edit”, and even though the test fails, I can see that the action was successfully done in Firefox ... but maybe a bit to late for Windmill.

What should I do to move thinkgs forward?

Many thanks!

Revision history for this message
Guilherme Salgado (salgado) wrote :

If the tests don't fail for you locally, I'll resubmit.

Revision history for this message
Adi Roiban (adiroiban) wrote :

It looks like the buildd-slavescanner error was fixed:
http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/revision/10455

I merged with devel and fixed the conflict.

I don't know how to fix those randomly failing Windmill tests.

Revision history for this message
Adi Roiban (adiroiban) wrote :

The active attribute for productseries is used in registry/model/product.py line 1015.

This was causing the failure of test_timeline_graph.

This is really strange, since the product.py code was commited long time ago and test_timeline_graph is not failing on mainline.

Can you please try to run this test on your computer using this branch?
These are the test failing on ec2-test, but they are fine on my computer.

bin/test --layer=WindmillLayer -t programming_languages_edit -t title_inline_edit -t product_edit_people_driver -t product_edit_people_owner -t project_licenses

Revision history for this message
Guilherme Salgado (salgado) wrote :

They fail here too, and the reason why they're failing is that on this
branch I asked you to move the active attribute from SeriesMixin to
DistroSeries because it seemed to be used only there. That was wrong --
ProductSeries inherits from SeriesMixin and, as you noticed, we have
code that rely on ProductSeries.attribute.

I think the correct fix here is to move the 'active' property back to
SeriesMixin. After you do that you'll have to update one of the
webservice tests for ProductSeries because its webservice representation
will now contain an extra 'active' field.

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (3.9 KiB)

Here is the diff for moving "active" back to SeriesMixin.

I have pushed these changes.

After pulling the changes, can you please try to run this test on you computer:
bin/test --layer=WindmillLayer -t programming_languages_edit -t title_inline_edit -t product_edit_people_driver -t product_edit_people_owner -t project_licenses -t test_timeline_graph

Many thanks!
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2010-03-09 12:37:25 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2010-03-12 17:49:47 +0000
@@ -165,12 +165,6 @@
             description=_(
                 "The title of this series. It should be distinctive "
                 "and designed to look good at the top of a page.")))
- active = exported(Bool(
- title=_("Active"),
- description=_(
- "Whether or not this series is stable and supported, or "
- "under current development. This excludes series which "
- "are experimental or obsolete.")))
     description = exported(
         Description(title=_("Description"), required=True,
             description=_("A detailed description of this series, with "

=== modified file 'lib/lp/registry/interfaces/series.py'
--- lib/lp/registry/interfaces/series.py 2010-03-08 09:34:22 +0000
+++ lib/lp/registry/interfaces/series.py 2010-03-12 17:50:01 +0000
@@ -92,6 +92,13 @@
 class ISeriesMixin(IHasDrivers):
     """Methods & properties shared between distro & product series."""

+ active = exported(Bool(
+ title=_("Active"),
+ description=_(
+ "Whether or not this series is stable and supported, or "
+ "under current development. This excludes series which "
+ "are experimental or obsolete.")))
+
     summary = exported(
         Summary(title=_("Summary"),
              description=_('A single paragraph that explains the goals of '
@@ -110,7 +117,6 @@
             readonly=True,
             value_type=Reference(schema=IPerson)))

-
     bug_supervisor = CollectionField(
         title=_('Currently just a reference to the parent bug '
                 'supervisor.'),

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-03-09 12:37:25 +0000
+++ lib/lp/registry/model/distroseries.py 2010-03-12 17:50:16 +0000
@@ -182,15 +182,6 @@
         intermediateTable='SectionSelection')

     @property
- def active(self):
- return self.status in [
- SeriesStatus.DEVELOPMENT,
- SeriesStatus.FROZEN,
- SeriesStatus.CURRENT,
- SeriesStatus.SUPPORTED,
- ]
-
- @property
     def named_version(self):
         return '%s (%s)' % (self.displayname, self.version)

=== modified file 'lib/lp/registry/model/series.py'
--- lib/lp/registry/model/series.py 2010-03-08 09:34:22 +0000
+++ lib/lp/registry/model/series.py 2010-03-12 17:50:23 +0000
@@ -25,6 +25,15 @@
     summary = StringCol(notNull=True)

     @property
+ def active(self):
+ return self.status in [
+ SeriesStatus.DEVELOPMENT,
+ SeriesStatus.FROZEN,
+ SeriesStatus.CURRENT,
+ ...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2010-03-08 10:43:34 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2010-03-15 15:04:36 +0000
@@ -12,7 +12,6 @@
12 'IDistroSeriesEditRestricted',12 'IDistroSeriesEditRestricted',
13 'IDistroSeriesPublic',13 'IDistroSeriesPublic',
14 'IDistroSeriesSet',14 'IDistroSeriesSet',
15 'ISeriesMixin',
16 'NoSuchDistroSeries',15 'NoSuchDistroSeries',
17 ]16 ]
1817
@@ -30,10 +29,9 @@
3029
31from canonical.launchpad import _30from canonical.launchpad import _
32from canonical.launchpad.fields import (31from canonical.launchpad.fields import (
33 ContentNameField, Description, PublicPersonChoice, Summary, Title,32 ContentNameField, Description, PublicPersonChoice, Title,
34 UniqueField)33 UniqueField)
35from canonical.launchpad.interfaces.launchpad import (34from canonical.launchpad.interfaces.launchpad import IHasAppointedDriver
36 IHasAppointedDriver, IHasDrivers)
37from canonical.launchpad.validators import LaunchpadValidationError35from canonical.launchpad.validators import LaunchpadValidationError
38from canonical.launchpad.validators.email import email_validator36from canonical.launchpad.validators.email import email_validator
39from canonical.launchpad.validators.name import name_validator37from canonical.launchpad.validators.name import name_validator
@@ -46,7 +44,7 @@
46 IBugTarget, IHasBugs, IHasOfficialBugTags)44 IBugTarget, IHasBugs, IHasOfficialBugTags)
47from lp.registry.interfaces.milestone import IHasMilestones, IMilestone45from lp.registry.interfaces.milestone import IHasMilestones, IMilestone
48from lp.registry.interfaces.role import IHasOwner46from lp.registry.interfaces.role import IHasOwner
49from lp.registry.interfaces.series import SeriesStatus47from lp.registry.interfaces.series import ISeriesMixin, SeriesStatus
50from lp.registry.interfaces.sourcepackage import ISourcePackage48from lp.registry.interfaces.sourcepackage import ISourcePackage
51from lp.registry.interfaces.structuralsubscription import (49from lp.registry.interfaces.structuralsubscription import (
52 IStructuralSubscriptionTarget)50 IStructuralSubscriptionTarget)
@@ -141,22 +139,10 @@
141 """Create a new milestone for this DistroSeries."""139 """Create a new milestone for this DistroSeries."""
142140
143141
144class ISeriesMixin(Interface):142class IDistroSeriesPublic(
145 """Methods & properties shared between distro & product series."""143 ISeriesMixin, IHasAppointedDriver, IHasOwner, IBugTarget,
146144 ISpecificationGoal, IHasMilestones, IHasOfficialBugTags,
147 active = exported(145 IHasBuildRecords):
148 Bool(
149 title=_("Active"),
150 description=_(
151 "Whether or not this series is stable and supported, or "
152 "under current development. This excludes series which "
153 "are experimental or obsolete.")))
154
155
156class IDistroSeriesPublic(IHasAppointedDriver, IHasDrivers, IHasOwner,
157 IBugTarget, ISpecificationGoal, IHasMilestones,
158 IHasBuildRecords, ISeriesMixin,
159 IHasOfficialBugTags):
160 """Public IDistroSeries properties."""146 """Public IDistroSeries properties."""
161147
162 id = Attribute("The distroseries's unique number.")148 id = Attribute("The distroseries's unique number.")
@@ -179,12 +165,6 @@
179 description=_(165 description=_(
180 "The title of this series. It should be distinctive "166 "The title of this series. It should be distinctive "
181 "and designed to look good at the top of a page.")))167 "and designed to look good at the top of a page.")))
182 summary = exported(
183 Summary(title=_("Summary"), required=True,
184 description=_(
185 "A brief summary of the highlights of this release. "
186 "It should be no longer than a single paragraph, up "
187 "to 200 words.")))
188 description = exported(168 description = exported(
189 Description(title=_("Description"), required=True,169 Description(title=_("Description"), required=True,
190 description=_("A detailed description of this series, with "170 description=_("A detailed description of this series, with "
@@ -251,14 +231,6 @@
251 nominatedarchindep = Attribute(231 nominatedarchindep = Attribute(
252 "DistroArchSeries designed to build architecture-independent "232 "DistroArchSeries designed to build architecture-independent "
253 "packages whithin this distroseries context.")233 "packages whithin this distroseries context.")
254 drivers = Attribute(
255 'A list of the people or teams who are drivers for this series. '
256 'This list is made up of any drivers or owners from this '
257 'DistroSeries, and the Distribution to which it belong.')
258 bug_supervisor = Attribute(
259 'Currently just a reference to the Distribution bug supervisor.')
260 security_contact = Attribute(
261 'Currently just a reference to the Distribution security contact.')
262 messagecount = Attribute("The total number of translatable items in "234 messagecount = Attribute("The total number of translatable items in "
263 "this series.")235 "this series.")
264 distroserieslanguages = Attribute("The set of dr-languages in this "236 distroserieslanguages = Attribute("The set of dr-languages in this "
@@ -445,14 +417,14 @@
445 """Return a list of packagings that are the most recently linked.417 """Return a list of packagings that are the most recently linked.
446418
447 At most five packages are returned of those most recently linked to an419 At most five packages are returned of those most recently linked to an
448 upstream.420 upstream.
449 """421 """
450422
451 @operation_parameters(423 @operation_parameters(
452 created_since_date=Datetime(424 created_since_date=Datetime(
453 title=_("Created Since Timestamp"),425 title=_("Created Since Timestamp"),
454 description=_("Return items that are more recent than this "426 description=_(
455 "timestamp."),427 "Return items that are more recent than this timestamp."),
456 required=False),428 required=False),
457 status=Choice(429 status=Choice(
458 # Really PackageUploadCustomFormat, patched in430 # Really PackageUploadCustomFormat, patched in
459431
=== modified file 'lib/lp/registry/interfaces/productseries.py'
--- lib/lp/registry/interfaces/productseries.py 2010-02-03 12:09:42 +0000
+++ lib/lp/registry/interfaces/productseries.py 2010-03-15 15:04:36 +0000
@@ -19,19 +19,16 @@
19from zope.interface import Interface, Attribute19from zope.interface import Interface, Attribute
2020
21from canonical.launchpad.fields import (21from canonical.launchpad.fields import (
22 ContentNameField, NoneableDescription, ParticipatingPersonChoice,22 ContentNameField, ParticipatingPersonChoice, Title)
23 PublicPersonChoice, Title)
24from lp.registry.interfaces.structuralsubscription import (23from lp.registry.interfaces.structuralsubscription import (
25 IStructuralSubscriptionTarget)24 IStructuralSubscriptionTarget)
26from lp.code.interfaces.branch import IBranch25from lp.code.interfaces.branch import IBranch
27from lp.bugs.interfaces.bugtarget import IBugTarget, IHasOfficialBugTags26from lp.bugs.interfaces.bugtarget import IBugTarget, IHasOfficialBugTags
28from lp.registry.interfaces.series import SeriesStatus27from lp.registry.interfaces.series import ISeriesMixin, SeriesStatus
29from canonical.launchpad.interfaces.launchpad import (28from canonical.launchpad.interfaces.launchpad import IHasAppointedDriver
30 IHasAppointedDriver, IHasDrivers)
31from lp.registry.interfaces.role import IHasOwner29from lp.registry.interfaces.role import IHasOwner
32from lp.registry.interfaces.milestone import (30from lp.registry.interfaces.milestone import (
33 IHasMilestones, IMilestone)31 IHasMilestones, IMilestone)
34from lp.registry.interfaces.person import IPerson
35from lp.registry.interfaces.productrelease import IProductRelease32from lp.registry.interfaces.productrelease import IProductRelease
36from lp.blueprints.interfaces.specificationtarget import (33from lp.blueprints.interfaces.specificationtarget import (
37 ISpecificationGoal)34 ISpecificationGoal)
@@ -92,9 +89,9 @@
92 """Create a new milestone for this ProjectSeries."""89 """Create a new milestone for this ProjectSeries."""
9390
9491
95class IProductSeriesPublic(IHasAppointedDriver, IHasDrivers, IHasOwner,92class IProductSeriesPublic(
96 IBugTarget, ISpecificationGoal, IHasMilestones,93 ISeriesMixin, IHasAppointedDriver, IHasOwner, IBugTarget,
97 IHasOfficialBugTags):94 ISpecificationGoal, IHasMilestones, IHasOfficialBugTags):
98 """Public IProductSeries properties."""95 """Public IProductSeries properties."""
99 # XXX Mark Shuttleworth 2004-10-14: Would like to get rid of id in96 # XXX Mark Shuttleworth 2004-10-14: Would like to get rid of id in
100 # interfaces, as soon as SQLobject allows using the object directly97 # interfaces, as soon as SQLobject allows using the object directly
@@ -158,15 +155,6 @@
158 'just returns the name.')),155 'just returns the name.')),
159 exported_as='display_name')156 exported_as='display_name')
160157
161 summary = exported(
162 NoneableDescription(title=_("Summary"),
163 description=_('A single paragraph that explains the goals of '
164 'of this series and the intended users. '
165 'For example: "The 2.0 series of Apache '
166 'represents the current stable series, '
167 'and is recommended for all new deployments".'),
168 required=True))
169
170 releases = exported(158 releases = exported(
171 CollectionField(159 CollectionField(
172 title=_("An iterator over the releases in this "160 title=_("An iterator over the releases in this "
@@ -201,28 +189,6 @@
201 readonly=True,189 readonly=True,
202 value_type=Reference(schema=IMilestone)))190 value_type=Reference(schema=IMilestone)))
203191
204 drivers = exported(
205 CollectionField(
206 title=_(
207 'A list of the people or teams who are drivers for this '
208 'series. This list is made up of any drivers or owners '
209 'from this project series, the project and if it exists, '
210 'the relevant project group.'),
211 readonly=True,
212 value_type=Reference(schema=IPerson)))
213
214 bug_supervisor = CollectionField(
215 title=_('Currently just a reference to the project bug '
216 'supervisor.'),
217 readonly=True,
218 value_type=Reference(schema=IPerson))
219
220 security_contact = PublicPersonChoice(
221 title=_('Security Contact'),
222 description=_('Currently just a reference to the project '
223 'security contact.'),
224 required=False, vocabulary='ValidPersonOrTeam')
225
226 branch = exported(192 branch = exported(
227 ReferenceChoice(193 ReferenceChoice(
228 title=_('Branch'),194 title=_('Branch'),
@@ -318,7 +284,6 @@
318 export_as_webservice_entry('project_series')284 export_as_webservice_entry('project_series')
319285
320286
321
322class IProductSeriesSet(Interface):287class IProductSeriesSet(Interface):
323 """Interface representing the set of ProductSeries."""288 """Interface representing the set of ProductSeries."""
324289
325290
=== modified file 'lib/lp/registry/interfaces/series.py'
--- lib/lp/registry/interfaces/series.py 2009-12-13 11:55:40 +0000
+++ lib/lp/registry/interfaces/series.py 2010-03-15 15:04:36 +0000
@@ -8,10 +8,22 @@
8__metaclass__ = type8__metaclass__ = type
99
10__all__ = [10__all__ = [
11 'SeriesStatus'11 'SeriesStatus',
12 'ISeriesMixin',
12 ]13 ]
1314
15from zope.schema import Bool
16
14from lazr.enum import DBEnumeratedType, DBItem17from lazr.enum import DBEnumeratedType, DBItem
18from lazr.restful.fields import CollectionField, Reference
19from lazr.restful.declarations import exported
20
21from canonical.launchpad import _
22from canonical.launchpad.fields import (
23 PublicPersonChoice, Summary)
24from canonical.launchpad.interfaces.launchpad import IHasDrivers
25from lp.registry.interfaces.person import IPerson
26
1527
16class SeriesStatus(DBEnumeratedType):28class SeriesStatus(DBEnumeratedType):
17 """Distro/Product Series Status29 """Distro/Product Series Status
@@ -76,3 +88,43 @@
76 haven't started working yet.88 haven't started working yet.
77 """)89 """)
7890
91
92class ISeriesMixin(IHasDrivers):
93 """Methods & properties shared between distro & product series."""
94
95 active = exported(Bool(
96 title=_("Active"),
97 description=_(
98 "Whether or not this series is stable and supported, or "
99 "under current development. This excludes series which "
100 "are experimental or obsolete.")))
101
102 summary = exported(
103 Summary(title=_("Summary"),
104 description=_('A single paragraph that explains the goals of '
105 'of this series and the intended users. '
106 'For example: "The 2.0 series of Apache '
107 'represents the current stable series, '
108 'and is recommended for all new deployments".'),
109 required=True))
110
111 drivers = exported(
112 CollectionField(
113 title=_(
114 'A list of the people or teams who are drivers for this '
115 'series. This list is made up of any drivers or owners '
116 'from this series and the parent drivers.'),
117 readonly=True,
118 value_type=Reference(schema=IPerson)))
119
120 bug_supervisor = CollectionField(
121 title=_('Currently just a reference to the parent bug '
122 'supervisor.'),
123 readonly=True,
124 value_type=Reference(schema=IPerson))
125
126 security_contact = PublicPersonChoice(
127 title=_('Security Contact'),
128 description=_('Currently just a reference to the parent '
129 'security contact.'),
130 required=False, vocabulary='ValidPersonOrTeam')
79131
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-03-08 10:43:34 +0000
+++ lib/lp/registry/model/distroseries.py 2010-03-15 15:04:36 +0000
@@ -10,7 +10,6 @@
10__all__ = [10__all__ = [
11 'DistroSeries',11 'DistroSeries',
12 'DistroSeriesSet',12 'DistroSeriesSet',
13 'SeriesMixin',
14 ]13 ]
1514
16import logging15import logging
@@ -87,8 +86,6 @@
87 HasSpecificationsMixin, Specification)86 HasSpecificationsMixin, Specification)
88from lp.translations.model.translationimportqueue import (87from lp.translations.model.translationimportqueue import (
89 HasTranslationImportsMixin)88 HasTranslationImportsMixin)
90from lp.registry.model.structuralsubscription import (
91 StructuralSubscriptionTargetMixin)
92from canonical.launchpad.helpers import shortlist89from canonical.launchpad.helpers import shortlist
93from lp.soyuz.interfaces.archive import (90from lp.soyuz.interfaces.archive import (
94 ALLOW_RELEASE_BUILDS, ArchivePurpose, IArchiveSet, MAIN_ARCHIVE_PURPOSES)91 ALLOW_RELEASE_BUILDS, ArchivePurpose, IArchiveSet, MAIN_ARCHIVE_PURPOSES)
@@ -98,7 +95,10 @@
98 IBinaryPackageName)95 IBinaryPackageName)
99from lp.registry.interfaces.series import SeriesStatus96from lp.registry.interfaces.series import SeriesStatus
100from lp.registry.interfaces.distroseries import (97from lp.registry.interfaces.distroseries import (
101 IDistroSeries, IDistroSeriesSet, ISeriesMixin)98 IDistroSeries, IDistroSeriesSet)
99from lp.registry.model.series import SeriesMixin
100from lp.registry.model.structuralsubscription import (
101 StructuralSubscriptionTargetMixin)
102from lp.translations.interfaces.languagepack import LanguagePackType102from lp.translations.interfaces.languagepack import LanguagePackType
103from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet103from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
104from lp.soyuz.interfaces.queue import PackageUploadStatus104from lp.soyuz.interfaces.queue import PackageUploadStatus
@@ -123,20 +123,6 @@
123 ISourcePackageFormatSelectionSet)123 ISourcePackageFormatSelectionSet)
124124
125125
126class SeriesMixin:
127 """See `ISeriesMixin`."""
128 implements(ISeriesMixin)
129
130 @property
131 def active(self):
132 return self.status in [
133 SeriesStatus.DEVELOPMENT,
134 SeriesStatus.FROZEN,
135 SeriesStatus.CURRENT,
136 SeriesStatus.SUPPORTED,
137 ]
138
139
140class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,126class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
141 HasTranslationImportsMixin, HasTranslationTemplatesMixin,127 HasTranslationImportsMixin, HasTranslationTemplatesMixin,
142 HasMilestonesMixin, SeriesMixin,128 HasMilestonesMixin, SeriesMixin,
@@ -154,7 +140,6 @@
154 name = StringCol(notNull=True)140 name = StringCol(notNull=True)
155 displayname = StringCol(notNull=True)141 displayname = StringCol(notNull=True)
156 title = StringCol(notNull=True)142 title = StringCol(notNull=True)
157 summary = StringCol(notNull=True)
158 description = StringCol(notNull=True)143 description = StringCol(notNull=True)
159 version = StringCol(notNull=True)144 version = StringCol(notNull=True)
160 status = EnumCol(145 status = EnumCol(
@@ -283,25 +268,6 @@
283 return self.distribution268 return self.distribution
284269
285 @property270 @property
286 def drivers(self):
287 """See `IDistroSeries`."""
288 drivers = set()
289 drivers.add(self.driver)
290 drivers = drivers.union(self.distribution.drivers)
291 drivers.discard(None)
292 return sorted(drivers, key=lambda driver: driver.displayname)
293
294 @property
295 def bug_supervisor(self):
296 """See `IDistroSeries`."""
297 return self.distribution.bug_supervisor
298
299 @property
300 def security_contact(self):
301 """See `IDistroSeries`."""
302 return self.distribution.security_contact
303
304 @property
305 def sortkey(self):271 def sortkey(self):
306 """A string to be used for sorting distro seriess.272 """A string to be used for sorting distro seriess.
307273
308274
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2010-03-05 23:59:18 +0000
+++ lib/lp/registry/model/productseries.py 2010-03-15 15:04:36 +0000
@@ -50,8 +50,6 @@
50from lp.registry.model.structuralsubscription import (50from lp.registry.model.structuralsubscription import (
51 StructuralSubscriptionTargetMixin)51 StructuralSubscriptionTargetMixin)
52from canonical.launchpad.helpers import shortlist52from canonical.launchpad.helpers import shortlist
53from lp.registry.interfaces.series import SeriesStatus
54from lp.registry.model.distroseries import SeriesMixin
55from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities53from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
56from lp.registry.interfaces.packaging import PackagingType54from lp.registry.interfaces.packaging import PackagingType
57from lp.translations.interfaces.potemplate import IHasTranslationTemplates55from lp.translations.interfaces.potemplate import IHasTranslationTemplates
@@ -60,10 +58,12 @@
60 SpecificationGoalStatus, SpecificationImplementationStatus,58 SpecificationGoalStatus, SpecificationImplementationStatus,
61 SpecificationSort)59 SpecificationSort)
62from canonical.launchpad.webapp.interfaces import NotFoundError60from canonical.launchpad.webapp.interfaces import NotFoundError
61from lp.registry.interfaces.series import SeriesStatus
63from lp.registry.interfaces.productseries import (62from lp.registry.interfaces.productseries import (
64 IProductSeries, IProductSeriesSet)63 IProductSeries, IProductSeriesSet)
65from lp.translations.interfaces.translations import (64from lp.translations.interfaces.translations import (
66 TranslationsBranchImportMode)65 TranslationsBranchImportMode)
66from lp.registry.model.series import SeriesMixin
67from canonical.launchpad.webapp.publisher import canonical_url67from canonical.launchpad.webapp.publisher import canonical_url
68from canonical.launchpad.webapp.sorting import sorted_dotted_numbers68from canonical.launchpad.webapp.sorting import sorted_dotted_numbers
6969
@@ -92,12 +92,12 @@
92 notNull=True, schema=SeriesStatus,92 notNull=True, schema=SeriesStatus,
93 default=SeriesStatus.DEVELOPMENT)93 default=SeriesStatus.DEVELOPMENT)
94 name = StringCol(notNull=True)94 name = StringCol(notNull=True)
95 summary = StringCol(notNull=True)
96 datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)95 datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
97 owner = ForeignKey(96 owner = ForeignKey(
98 dbName="owner", foreignKey="Person",97 dbName="owner", foreignKey="Person",
99 storm_validator=validate_person_not_private_membership,98 storm_validator=validate_person_not_private_membership,
100 notNull=True)99 notNull=True)
100
101 driver = ForeignKey(101 driver = ForeignKey(
102 dbName="driver", foreignKey="Person",102 dbName="driver", foreignKey="Person",
103 storm_validator=validate_person_not_private_membership,103 storm_validator=validate_person_not_private_membership,
@@ -165,25 +165,6 @@
165 """See `IHasBugs`."""165 """See `IHasBugs`."""
166 return self.product.max_bug_heat166 return self.product.max_bug_heat
167167
168 @property
169 def drivers(self):
170 """See IProductSeries."""
171 drivers = set()
172 drivers.add(self.driver)
173 drivers = drivers.union(self.product.drivers)
174 drivers.discard(None)
175 return sorted(drivers, key=lambda x: x.displayname)
176
177 @property
178 def bug_supervisor(self):
179 """See IProductSeries."""
180 return self.product.bug_supervisor
181
182 @property
183 def security_contact(self):
184 """See IProductSeries."""
185 return self.product.security_contact
186
187 def getPOTemplate(self, name):168 def getPOTemplate(self, name):
188 """See IProductSeries."""169 """See IProductSeries."""
189 return POTemplate.selectOne(170 return POTemplate.selectOne(
@@ -308,7 +289,7 @@
308289
309 # filter based on completion. see the implementation of290 # filter based on completion. see the implementation of
310 # Specification.is_complete() for more details291 # Specification.is_complete() for more details
311 completeness = Specification.completeness_clause292 completeness = Specification.completeness_clause
312293
313 if SpecificationFilter.COMPLETE in filter:294 if SpecificationFilter.COMPLETE in filter:
314 query += ' AND ( %s ) ' % completeness295 query += ' AND ( %s ) ' % completeness
@@ -438,8 +419,8 @@
438419
439 def getTranslationTemplates(self):420 def getTranslationTemplates(self):
440 """See `IHasTranslationTemplates`."""421 """See `IHasTranslationTemplates`."""
441 result = POTemplate.selectBy(productseries=self,422 result = POTemplate.selectBy(
442 orderBy=['-priority','name'])423 productseries=self, orderBy=['-priority', 'name'])
443 return shortlist(result, 300)424 return shortlist(result, 300)
444425
445 def getCurrentTranslationTemplates(self, just_ids=False):426 def getCurrentTranslationTemplates(self, just_ids=False):
@@ -472,7 +453,7 @@
472 ProductSeries.product = Product.id AND453 ProductSeries.product = Product.id AND
473 (iscurrent IS FALSE OR Product.official_rosetta IS FALSE)454 (iscurrent IS FALSE OR Product.official_rosetta IS FALSE)
474 ''' % sqlvalues(self),455 ''' % sqlvalues(self),
475 orderBy=['-priority','name'],456 orderBy=['-priority', 'name'],
476 clauseTables = ['ProductSeries', 'Product'])457 clauseTables = ['ProductSeries', 'Product'])
477 return shortlist(result, 300)458 return shortlist(result, 300)
478459
479460
=== added file 'lib/lp/registry/model/series.py'
--- lib/lp/registry/model/series.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/series.py 2010-03-15 15:04:36 +0000
@@ -0,0 +1,53 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Common implementations for a series."""
5
6__metaclass__ = type
7
8__all__ = [
9 'SeriesMixin',
10 ]
11
12from operator import attrgetter
13from sqlobject import StringCol
14
15from zope.interface import implements
16
17from lp.registry.interfaces.series import ISeriesMixin, SeriesStatus
18
19
20class SeriesMixin:
21 """See `ISeriesMixin`."""
22
23 implements(ISeriesMixin)
24
25 summary = StringCol(notNull=True)
26
27 @property
28 def active(self):
29 return self.status in [
30 SeriesStatus.DEVELOPMENT,
31 SeriesStatus.FROZEN,
32 SeriesStatus.CURRENT,
33 SeriesStatus.SUPPORTED,
34 ]
35
36 @property
37 def bug_supervisor(self):
38 """See `ISeriesMixin`."""
39 return self.parent.bug_supervisor
40
41 @property
42 def security_contact(self):
43 """See `ISeriesMixin`."""
44 return self.parent.security_contact
45
46 @property
47 def drivers(self):
48 """See `ISeriesMixin`."""
49 drivers = set()
50 drivers.add(self.driver)
51 drivers = drivers.union(self.parent.drivers)
52 drivers.discard(None)
53 return sorted(drivers, key=attrgetter('displayname'))
054
=== modified file 'lib/lp/registry/stories/webservice/xx-distroseries.txt'
--- lib/lp/registry/stories/webservice/xx-distroseries.txt 2010-02-04 21:16:37 +0000
+++ lib/lp/registry/stories/webservice/xx-distroseries.txt 2010-03-15 15:04:36 +0000
@@ -68,6 +68,7 @@
68 displayname: u'Hoary'68 displayname: u'Hoary'
69 distribution_link: u'http://.../ubuntu'69 distribution_link: u'http://.../ubuntu'
70 driver_link: None70 driver_link: None
71 drivers_collection_link: u'http://.../ubuntu/hoary/drivers'
71 fullseriesname: u'Ubuntu Hoary'72 fullseriesname: u'Ubuntu Hoary'
72 main_archive_link: u'http://.../ubuntu/+archive/primary'73 main_archive_link: u'http://.../ubuntu/+archive/primary'
73 name: u'hoary'74 name: u'hoary'
7475
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-02-04 21:18:35 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-03-15 15:04:36 +0000
@@ -93,7 +93,8 @@
93active_milestones_collection_link and the93active_milestones_collection_link and the
94all_milestones_collection_link.94all_milestones_collection_link.
9595
96 >>> response = webservice.get(mozilla['active_milestones_collection_link'])96 >>> response = webservice.get(
97 ... mozilla['active_milestones_collection_link'])
97 >>> active_milestones = response.jsonBody()98 >>> active_milestones = response.jsonBody()
98 >>> print_self_link_of_entries(active_milestones)99 >>> print_self_link_of_entries(active_milestones)
99 http://.../mozilla/+milestone/1.0100 http://.../mozilla/+milestone/1.0
@@ -233,7 +234,8 @@
233active_milestones_collection_link and the234active_milestones_collection_link and the
234all_milestones_collection_link.235all_milestones_collection_link.
235236
236 >>> response = webservice.get(firefox['active_milestones_collection_link'])237 >>> response = webservice.get(
238 ... firefox['active_milestones_collection_link'])
237 >>> active_milestones = response.jsonBody()239 >>> active_milestones = response.jsonBody()
238 >>> print_self_link_of_entries(active_milestones)240 >>> print_self_link_of_entries(active_milestones)
239 http://.../firefox/+milestone/1.0241 http://.../firefox/+milestone/1.0
@@ -360,11 +362,14 @@
360 >>> # Create a product with a series and release.362 >>> # Create a product with a series and release.
361 >>> login('test@canonical.com')363 >>> login('test@canonical.com')
362 >>> test_project_owner = factory.makePerson(name='test-project-owner')364 >>> test_project_owner = factory.makePerson(name='test-project-owner')
363 >>> test_project = factory.makeProduct(name='test-project', owner=test_project_owner)365 >>> test_project = factory.makeProduct(
366 ... name='test-project', owner=test_project_owner)
364 >>> test_series = factory.makeProductSeries(367 >>> test_series = factory.makeProductSeries(
365 ... product=test_project, name='test-series', owner=test_project_owner)368 ... product=test_project, name='test-series',
369 ... owner=test_project_owner)
366 >>> test_milestone = factory.makeMilestone(370 >>> test_milestone = factory.makeMilestone(
367 ... product=test_project, name='test-milestone', productseries=test_series)371 ... product=test_project, name='test-milestone',
372 ... productseries=test_series)
368 >>> test_project_release = factory.makeProductRelease(373 >>> test_project_release = factory.makeProductRelease(
369 ... product=test_project, milestone=test_milestone)374 ... product=test_project, milestone=test_milestone)
370 >>> logout()375 >>> logout()
@@ -500,7 +505,8 @@
500 >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()505 >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()
501506
502 >>> project_entries = sorted(507 >>> project_entries = sorted(
503 ... project_collection['entries'], key=itemgetter('display_name'))508 ... project_collection['entries'],
509 ... key=itemgetter('display_name','name'))
504 >>> len(project_entries)510 >>> len(project_entries)
505 25511 25
506512
@@ -528,7 +534,8 @@
528If you don't specify "text" to the search a batched list of all the534If you don't specify "text" to the search a batched list of all the
529projects is returned.535projects is returned.
530536
531 >>> project_collection = webservice.named_get("/projects", "search").jsonBody()537 >>> project_collection = webservice.named_get(
538 ... "/projects", "search").jsonBody()
532 >>> len(project_collection['entries'])539 >>> len(project_collection['entries'])
533 5540 5
534541
@@ -810,6 +817,7 @@
810817
811 >>> babadoo_foobadoo = webservice.get('/babadoo/foobadoo').jsonBody()818 >>> babadoo_foobadoo = webservice.get('/babadoo/foobadoo').jsonBody()
812 >>> pprint_entry(babadoo_foobadoo)819 >>> pprint_entry(babadoo_foobadoo)
820 active: True
813 active_milestones_collection_link: u'http://.../babadoo/foobadoo/active_milestones'821 active_milestones_collection_link: u'http://.../babadoo/foobadoo/active_milestones'
814 all_milestones_collection_link: u'http://.../babadoo/foobadoo/all_milestones'822 all_milestones_collection_link: u'http://.../babadoo/foobadoo/all_milestones'
815 branch_link: u'http://.../~babadoo-owner/babadoo/fooey'823 branch_link: u'http://.../~babadoo-owner/babadoo/fooey'