← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/dds-stats-portlet into lp:launchpad

 

Raphaël Victor Badin has proposed merging lp:~rvb/launchpad/dds-stats-portlet into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #758493 in Launchpad itself: "The index page of a derived distroseries should display a portlet with links to the differences-with-parent pages."
  https://bugs.launchpad.net/launchpad/+bug/758493

For more details, see:
https://code.launchpad.net/~rvb/launchpad/dds-stats-portlet/+merge/57280

This branch adds a portlet (behind the 'soyuz.derived-series-ui.enabled' feature flag) to the distroseries home page. It displays stats about the differences with the parent along with links to the differences pages: +localpackagediffs, +uniquepackages and +missingpackages.

= Tests =
./bin/test -cvv test_distroseries test_is_initialising
./bin/test -cvv test_distroseries test_is_derived
./bin/test -cvv test_series_views test_num_differences
./bin/test -cvv test_series_views test_num_differences_in_parent
./bin/test -cvv test_series_views test_num_differences_in_child
./bin/test -cvv test_series_views test_differences_portlet_all_differences
./bin/test -cvv test_series_views test_differences_no_flag_no_portlet
./bin/test -cvv test_series_views test_differences_portlet_initialising
./bin/test -cvv test_series_views test_differences_portlet_no_differences
./bin/test -cvv -t xx-derivedistroseries.txt

= QA =
On dogfood:
The portlet should appear on this page:
https://dogfood.launchpad.net/ubuntu/maverick
And the numbers it will provide should be consistent with the actual number of differences:
https://dogfood.launchpad.net/ubuntu/maverick/+localpackagediffs
https://dogfood.launchpad.net/ubuntu/maverick/+uniquepackages
https://dogfood.launchpad.net/ubuntu/maverick/+missingpackages
-- 
https://code.launchpad.net/~rvb/launchpad/dds-stats-portlet/+merge/57280
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/dds-stats-portlet into lp:launchpad.
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2011-04-08 07:43:39 +0000
+++ lib/lp/registry/browser/configure.zcml	2011-04-12 16:26:33 +0000
@@ -112,7 +112,11 @@
             name="+portlet-package-summary"
             facet="overview"
             template="../templates/distroseries-portlet-packaging.pt"/>
-    </browser:pages>
+        <browser:page
+            name="+portlet-derivation"
+            facet="overview"
+            template="../templates/distroseries-portlet-derivation.pt"/>
+     </browser:pages>
     <browser:page
         for="lp.registry.interfaces.distroseries.IDistroSeries"
         class="lp.registry.browser.distroseries.DistroSeriesPackagesView"

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-04-06 20:04:35 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-04-12 16:26:33 +0000
@@ -409,6 +409,29 @@
     def milestone_batch_navigator(self):
         return BatchNavigator(self.context.all_milestones, self.request)
 
+    def _num_differences(self, difference_type):
+        differences = getUtility(
+            IDistroSeriesDifferenceSource).getForDistroSeries(
+                self.context,
+                difference_type=difference_type,
+                status=(DistroSeriesDifferenceStatus.NEEDS_ATTENTION,))
+        return differences.count()
+
+    @cachedproperty
+    def num_differences(self):
+        return self._num_differences(
+            DistroSeriesDifferenceType.DIFFERENT_VERSIONS)
+
+    @cachedproperty
+    def num_differences_in_parent(self):
+        return self._num_differences(
+            DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES)
+
+    @cachedproperty
+    def num_differences_in_child(self):
+        return self._num_differences(
+            DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES)
+
 
 class DistroSeriesEditView(LaunchpadEditFormView, SeriesStatusMixin):
     """View class that lets you edit a DistroSeries object.

=== modified file 'lib/lp/registry/browser/tests/test_series_views.py'
--- lib/lp/registry/browser/tests/test_series_views.py	2011-04-06 07:52:33 +0000
+++ lib/lp/registry/browser/tests/test_series_views.py	2011-04-12 16:26:33 +0000
@@ -8,7 +8,10 @@
 from BeautifulSoup import BeautifulSoup
 import soupmatchers
 from storm.zope.interfaces import IResultSet
-from testtools.matchers import EndsWith
+from testtools.matchers import (
+    EndsWith,
+    Not,
+    )
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -33,6 +36,7 @@
     DistroSeriesDifferenceType,
     )
 from lp.services.features import (
+    get_relevant_feature_controller,
     getFeatureFlag,
     install_feature_controller,
     )
@@ -45,6 +49,9 @@
     PackagePublishingStatus,
     SourcePackageFormat,
     )
+from lp.soyuz.interfaces.distributionjob import (
+    IInitialiseDistroSeriesJobSource,
+    )
 from lp.soyuz.interfaces.sourcepackageformat import (
     ISourcePackageFormatSelectionSet,
     )
@@ -85,6 +92,30 @@
         view = create_initialized_view(distroseries, '+index')
         self.assertEqual(view.needs_linking, None)
 
+    def _createDifferenceAndGetView(self, difference_type):
+        # Helper function to create a valid DSD.
+        distroseries = self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+        ds_diff = self.factory.makeDistroSeriesDifference(
+            derived_series=distroseries, difference_type=difference_type)
+        view = create_initialized_view(distroseries, '+index')
+        return view
+
+    def test_num_differences(self):
+        diff_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
+        view = self._createDifferenceAndGetView(diff_type)
+        self.assertEqual(1, view.num_differences)
+
+    def test_num_differences_in_parent(self):
+        diff_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+        view = self._createDifferenceAndGetView(diff_type)
+        self.assertEqual(1, view.num_differences_in_parent)
+
+    def test_num_differences_in_child(self):
+        diff_type = DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES
+        view = self._createDifferenceAndGetView(diff_type)
+        self.assertEqual(1, view.num_differences_in_child)
+
 
 def set_derived_series_ui_feature_flag(test_case):
     # Helper to set the feature flag enabling the derived series ui.
@@ -101,6 +132,145 @@
     test_case.addCleanup(install_feature_controller, None)
 
 
+class DistroSeriesIndexFunctionalTestCase(TestCaseWithFactory):
+    """Test the distroseries +index page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def _setupDifferences(self, name, parent_name, nb_diff_versions,
+                          nb_diff_child, nb_diff_parent):
+        # Helper to create DSD of the different types.
+        derived_series = self.factory.makeDistroSeries(
+            name=name,
+            parent_series=self.factory.makeDistroSeries(name=parent_name))
+        self.simple_user = self.factory.makePerson()
+        for i in range(nb_diff_versions):
+            diff_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
+            self.factory.makeDistroSeriesDifference(
+                derived_series=derived_series,
+                difference_type=diff_type)
+        for i in range(nb_diff_child):
+            diff_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+            self.factory.makeDistroSeriesDifference(
+                derived_series=derived_series,
+                difference_type=diff_type)
+        for i in range(nb_diff_parent):
+            diff_type = DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES
+            self.factory.makeDistroSeriesDifference(
+                derived_series=derived_series,
+                difference_type=diff_type)
+        return derived_series
+
+    def test_differences_no_flag_no_portlet(self):
+        # The portlet is not displayed if the feature flag is not enabled.
+        derived_series = self._setupDifferences('deri', 'sid', 1, 2, 2)
+        portlet_header = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Derivation portlet header', 'h2',
+                text='Derived from Sid'),
+            )
+
+        with person_logged_in(self.simple_user):
+            view = create_initialized_view(
+                derived_series,
+                '+index',
+                principal=self.simple_user)
+            html = view()
+
+        self.assertEqual(
+            None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
+        self.assertThat(html, Not(portlet_header))
+
+    def test_differences_portlet_all_differences(self):
+        # The difference portlet shows the differences with the parent
+        # series.
+        set_derived_series_ui_feature_flag(self)
+        derived_series = self._setupDifferences('deri', 'sid', 1, 2, 3)
+        portlet_display = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Derivation portlet header', 'h2',
+                text='Derived from Sid'),
+            soupmatchers.Tag(
+                'Differences link', 'a',
+                text=re.compile('\s*1 package with differences.\s*'),
+                attrs={'href': re.compile('.*/\+localpackagediffs')}),
+            soupmatchers.Tag(
+                'Parent diffs link', 'a',
+                text=re.compile('\s*2 packages in Sid.\s*'),
+                attrs={'href': re.compile('.*/\+missingpackages')}),
+            soupmatchers.Tag(
+                'Child diffs link', 'a',
+                text=re.compile('\s*3 packages in Deri.\s*'),
+                attrs={'href': re.compile('.*/\+uniquepackages')}))
+
+        with person_logged_in(self.simple_user):
+            view = create_initialized_view(
+                derived_series,
+                '+index',
+                principal=self.simple_user)
+            # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
+            # self.features to NullFeatureController.
+            view.request.features = get_relevant_feature_controller()
+            html = view()
+
+        self.assertThat(html, portlet_display)
+
+    def test_differences_portlet_no_differences(self):
+        # The difference portlet displays 'No differences' if there is no
+        # differences with the parent.
+        set_derived_series_ui_feature_flag(self)
+        derived_series = self._setupDifferences('deri', 'sid', 0, 0, 0)
+        portlet_display = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Derivation portlet header', 'h2',
+                text='Derived from Sid'),
+            soupmatchers.Tag(
+                'Child diffs link', True,
+                text=re.compile('\s*No differences\s*')),
+              )
+
+        with person_logged_in(self.simple_user):
+            view = create_initialized_view(
+                derived_series,
+                '+index',
+                principal=self.simple_user)
+            # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
+            # self.features to NullFeatureController.
+            view.request.features = get_relevant_feature_controller()
+            html = view()
+
+        self.assertThat(html, portlet_display)
+
+    def test_differences_portlet_initialising(self):
+        # The difference portlet displays 'The series is initialising.' if
+        # there is an initialising job for the series.
+        set_derived_series_ui_feature_flag(self)
+        derived_series = self._setupDifferences('deri', 'sid', 0, 0, 0)
+        job_source = getUtility(IInitialiseDistroSeriesJobSource)
+        job = job_source.create(derived_series.parent, derived_series)
+        portlet_display = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Derived series', 'h2',
+                text='Derived series'),
+            soupmatchers.Tag(
+                'Init message', True,
+                text=re.compile('\s*This series is initialising.\s*')),
+              )
+
+        with person_logged_in(self.simple_user):
+            view = create_initialized_view(
+                derived_series,
+                '+index',
+                principal=self.simple_user)
+            # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
+            # self.features to NullFeatureController.
+            view.request.features = get_relevant_feature_controller()
+            html = view()
+
+        self.assertTrue(derived_series.is_initialising)
+        self.assertThat(html, portlet_display)
+
+
 class DistroSeriesDifferenceMixin():
     """A helper class for testing differences pages"""
 

=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py	2011-04-04 01:42:16 +0000
+++ lib/lp/registry/interfaces/distroseries.py	2011-04-12 16:26:33 +0000
@@ -235,6 +235,12 @@
         Choice(
             title=_("Status"), required=True,
             vocabulary=SeriesStatus))
+    is_derived_series = Bool(
+        title=u'Is this series a derived series?', readonly=True,
+        description=(u"Whether or not this series is a derived series."))
+    is_initialising = Bool(
+        title=u'Is this series initialising?', readonly=True,
+        description=(u"Whether or not this series is initialising."))
     datereleased = exported(
         Datetime(title=_("Date released")))
     parent_series = exported(

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2011-04-12 13:17:34 +0000
+++ lib/lp/registry/model/distribution.py	2011-04-12 16:26:33 +0000
@@ -602,7 +602,7 @@
     def derivatives(self):
         """See `IDistribution`."""
         ParentDistroSeries = ClassAlias(DistroSeries)
-        # rvb 2011-04-08 bug=754750: The clause
+        # XXX rvb 2011-04-08 bug=754750: The clause
         # 'DistroSeries.distributionID!=self.id' is only required
         # because the parent_series attribute has been (mis-)used
         # to denote other relations than proper derivation

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2011-04-12 13:17:34 +0000
+++ lib/lp/registry/model/distroseries.py	2011-04-12 16:26:33 +0000
@@ -67,9 +67,7 @@
     MAIN_STORE,
     SLAVE_FLAVOR,
     )
-from lp.app.enums import (
-    service_uses_launchpad,
-    )
+from lp.app.enums import service_uses_launchpad
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import IServiceUsage
 from lp.blueprints.enums import (
@@ -124,6 +122,7 @@
 from lp.registry.model.series import SeriesMixin
 from lp.registry.model.sourcepackage import SourcePackage
 from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.job.model.job import Job
 from lp.services.propertycache import (
     cachedproperty,
     get_property_cache,
@@ -788,6 +787,20 @@
             self.distribution.name.capitalize(), self.name.capitalize())
 
     @property
+    def is_derived_series(self):
+        """See `IDistroSeries`."""
+        # XXX rvb 2011-04-11 bug=754750: This should be cleaned up once
+        # the bug is fixed.
+        return self.parent_series is not None
+
+    @property
+    def is_initialising(self):
+        """See `IDistroSeries`."""
+        return not getUtility(
+            IInitialiseDistroSeriesJobSource).getJobs(
+                self, statuses=Job.PENDING_STATUSES).is_empty()
+
+    @property
     def bugtargetname(self):
         """See IBugTarget."""
         # XXX mpt 2007-07-10 bugs 113258, 113262:
@@ -1990,7 +2003,7 @@
 
     def getDerivedSeries(self):
         """See `IDistroSeriesPublic`."""
-        # rvb 2011-04-08 bug=754750: The clause
+        # XXX rvb 2011-04-08 bug=754750: The clause
         # 'DistroSeries.distributionID!=self.distributionID' is only
         # required because the parent_series attribute has been
         # (mis-)used to denote other relations than proper derivation

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-04-06 22:51:53 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-04-12 16:26:33 +0000
@@ -106,7 +106,7 @@
     @staticmethod
     def new(derived_series, source_package_name):
         """See `IDistroSeriesDifferenceSource`."""
-        if derived_series.parent_series is None:
+        if not derived_series.is_derived_series:
             raise NotADerivedSeriesError()
 
         store = IMasterStore(DistroSeriesDifference)

=== modified file 'lib/lp/registry/stories/webservice/xx-derivedistroseries.txt'
--- lib/lp/registry/stories/webservice/xx-derivedistroseries.txt	2011-03-28 10:42:08 +0000
+++ lib/lp/registry/stories/webservice/xx-derivedistroseries.txt	2011-04-12 16:26:33 +0000
@@ -100,3 +100,13 @@
     ...     print job.distroseries.name
     child1
     child2
+
+The jobs can also be queried per distribution.
+
+    >>> from lp.services.job.model.job import Job
+    >>> jobs = getUtility(
+    ...    IInitialiseDistroSeriesJobSource).getJobs(
+    ...        child_series, statuses=Job.PENDING_STATUSES)
+    >>> for job in jobs:
+    ...     print job.distroseries.name
+    child1

=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
--- lib/lp/registry/templates/distroseries-index.pt	2011-03-30 21:38:03 +0000
+++ lib/lp/registry/templates/distroseries-index.pt	2011-04-12 16:26:33 +0000
@@ -62,6 +62,10 @@
           <div
             tal:replace="structure context/@@+portlet-details"/>
         </div>
+        <div class="yui-u"
+             tal:condition="request/features/soyuz.derived-series-ui.enabled">
+          <div tal:replace="structure context/@@+portlet-derivation" />
+        </div>
 
         <div class="yui-u">
           <div tal:replace="structure context/@@+portlet-package-summary" />

=== added file 'lib/lp/registry/templates/distroseries-portlet-derivation.pt'
--- lib/lp/registry/templates/distroseries-portlet-derivation.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/distroseries-portlet-derivation.pt	2011-04-12 16:26:33 +0000
@@ -0,0 +1,50 @@
+<div
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  id="series-derivation" class="portlet"
+  tal:define="overview_menu context/menu:overview">
+  <tal:is_derived condition="context/is_derived_series">
+    <tal:is_initialised condition="not: context/is_initialising">
+    <h2>Derived from <tal:name replace="context/parent_series/displayname"/></h2>
+
+      <tal:diffs define="nb_diffs view/num_differences;
+                         nb_diffs_in_parent view/num_differences_in_parent;
+                         nb_diffs_in_child view/num_differences_in_child;">
+        <ul id="derivation_stats">
+          <li class="sprite info" tal:condition="nb_diffs">
+            <a tal:attributes="href string:${context/fmt:url}/+localpackagediffs">
+              <tal:nb_diffs replace="nb_diffs"/> package<tal:plural
+                content="string:s"
+                condition="python:nb_diffs!=1"/> with differences.
+            </a>
+          </li>
+         <li class="sprite info" tal:condition="nb_diffs_in_parent">
+            <a tal:attributes="href string:${context/fmt:url}/+missingpackages">
+              <tal:nb_diffs replace="nb_diffs_in_parent"/> package<tal:plural
+                content="string:s"
+                condition="python:nb_diffs_in_parent!=1"/> in <tal:replace
+                  replace="context/parent_series/displayname">Sid</tal:replace>.
+            </a>
+          </li>
+          <li class="sprite info" tal:condition="nb_diffs_in_child">
+            <a tal:attributes="href string:${context/fmt:url}/+uniquepackages">
+              <tal:nb_diffs replace="nb_diffs_in_child"/> package<tal:plural
+                content="string:s"
+                condition="python:nb_diffs_in_child!=1"/> in <tal:replace
+                  replace="context/displayname">Natty</tal:replace>.
+            </a>
+          </li>
+         </ul>
+        <tal:no_diffs
+          condition="python:not(nb_diffs or nb_diffs_in_parent or nb_diffs_in_child)">
+          No differences.
+        </tal:no_diffs>
+      </tal:diffs>
+  </tal:is_initialised>
+  <tal:is_initialising condition="context/is_initialising">
+    <h2>Derived series</h2>
+      This series is initialising.
+  </tal:is_initialising>
+  </tal:is_derived>
+</div>

=== modified file 'lib/lp/registry/tests/test_distroseries.py'
--- lib/lp/registry/tests/test_distroseries.py	2011-03-15 12:43:01 +0000
+++ lib/lp/registry/tests/test_distroseries.py	2011-04-12 16:26:33 +0000
@@ -26,6 +26,9 @@
     )
 from lp.soyuz.interfaces.archive import IArchiveSet
 from lp.soyuz.interfaces.component import IComponentSet
+from lp.soyuz.interfaces.distributionjob import (
+    IInitialiseDistroSeriesJobSource,
+    )
 from lp.soyuz.interfaces.distroseriessourcepackagerelease import (
     IDistroSeriesSourcePackageRelease,
     )
@@ -212,14 +215,38 @@
             [distroseries], distroseries.parent_series.getDerivedSeries())
 
     def test_registrant_owner_differ(self):
-        # The registrant is the creator whereas the owner is the distribution's
-        # owner
+        # The registrant is the creator whereas the owner is the
+        # distribution's owner.
         registrant = self.factory.makePerson()
         distroseries = self.factory.makeDistroRelease(registrant=registrant)
         self.assertEquals(distroseries.distribution.owner, distroseries.owner)
         self.assertEquals(registrant, distroseries.registrant)
         self.assertNotEqual(distroseries.registrant, distroseries.owner)
 
+    def test_is_derived(self):
+        # The series is a derived series if it has a parent_series set.
+        derived_distroseries = self.factory.makeDistroRelease(
+            parent_series=self.factory.makeDistroRelease())
+        distroseries = self.factory.makeDistroRelease()
+        self.assertEquals(False, distroseries.is_derived_series)
+        self.assertEquals(True, derived_distroseries.is_derived_series)
+
+    def test_is_initialising(self):
+        # The series is_initialising only if there is an initialisation
+        # job with a pending status attached to this series.
+        distroseries = self.factory.makeDistroRelease()
+        self.assertEquals(False, distroseries.is_initialising)
+        job_source = getUtility(IInitialiseDistroSeriesJobSource)
+        job = job_source.create(distroseries.parent, distroseries)
+        self.assertEquals(True, distroseries.is_initialising)
+        job.start()
+        self.assertEquals(True, distroseries.is_initialising)
+        job.queue()
+        self.assertEquals(True, distroseries.is_initialising)
+        job.start()
+        job.complete()
+        self.assertEquals(False, distroseries.is_initialising)
+
 
 class TestDistroSeriesPackaging(TestCaseWithFactory):
 

=== modified file 'lib/lp/soyuz/interfaces/distributionjob.py'
--- lib/lp/soyuz/interfaces/distributionjob.py	2011-03-18 01:17:42 +0000
+++ lib/lp/soyuz/interfaces/distributionjob.py	2011-04-12 16:26:33 +0000
@@ -92,6 +92,11 @@
     def create(distroseries, arches, packagesets, rebuild):
         """Create a new initialisation job for a distroseries."""
 
+    def getJobs(distroseries, statuses):
+        """Retrieve initialisation jobs with specified statuses
+        for a distroseries.
+        """
+
 
 class ISyncPackageJobSource(IJobSource):
     """An interface for acquiring IISyncPackageJobs."""

=== modified file 'lib/lp/soyuz/model/initialisedistroseriesjob.py'
--- lib/lp/soyuz/model/initialisedistroseriesjob.py	2011-03-24 14:53:01 +0000
+++ lib/lp/soyuz/model/initialisedistroseriesjob.py	2011-04-12 16:26:33 +0000
@@ -11,7 +11,6 @@
     classProvides,
     implements,
     )
-
 from canonical.launchpad.interfaces.lpstorm import (
     IMasterStore,
     IStore,
@@ -27,6 +26,7 @@
     DistributionJobDerived,
     )
 from lp.soyuz.scripts.initialise_distroseries import InitialiseDistroSeries
+from lp.services.job.model.job import Job
 
 
 class InitialiseDistroSeriesJob(DistributionJobDerived):
@@ -51,6 +51,17 @@
         IMasterStore(DistributionJob).add(job)
         return cls(job)
 
+    @classmethod
+    def getJobs(cls, distroseries, statuses):
+        """See `IInitialiseDistroSeriesJob`."""
+        return IStore(DistributionJob).find(
+            DistributionJob,
+            DistributionJob.job_id == Job.id,
+            DistributionJob.job_type ==
+                DistributionJobType.INITIALISE_SERIES,
+            DistributionJob.distroseries_id == distroseries.id,
+            Job._status.is_in(statuses))
+
     @property
     def parent(self):
         return IStore(DistroSeries).get(