← Back to team overview

launchpad-reviewers team mailing list archive

lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #727105 Add new Distribution:+initseries page.
  https://bugs.launchpad.net/bugs/727105

For more details, see:
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab/+merge/52471

This branch adds a named vocabulary ("DistroSeriesDerivation") that
contains the set of distroseries that the context distroseries or
distribution can derive from.

It also fixes adapters for IProductSeries -> IProduct and
IDistroSeries -> IDistribution. The code already exists in the tree
but was not registered or tested.

This branch will not be committed; it is part of a larger change.

-- 
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab/+merge/52471
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab into lp:launchpad.
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2011-02-16 21:34:13 +0000
+++ lib/lp/registry/configure.zcml	2011-03-08 14:48:38 +0000
@@ -238,6 +238,11 @@
         factory="lp.registry.adapters.distroseries_to_distribution"
         permission="zope.Public"/>
     <adapter
+        provides="lp.registry.interfaces.distribution.IDistribution"
+        for="lp.registry.interfaces.distroseries.IDistroSeries"
+        factory="lp.registry.adapters.distroseries_to_distribution"
+        permission="zope.Public"/>
+    <adapter
         provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.distroseries.IDistroSeries"
         factory="lp.registry.browser.distroseries.DistroSeriesBreadcrumb"
@@ -1425,6 +1430,11 @@
         factory="lp.registry.adapters.productseries_to_product"
         permission="zope.Public"/>
     <adapter
+        provides="lp.registry.interfaces.product.IProduct"
+        for="lp.registry.interfaces.productseries.IProductSeries"
+        factory="lp.registry.adapters.productseries_to_product"
+        permission="zope.Public"/>
+    <adapter
         provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.productseries.IProductSeries"
         factory="lp.registry.browser.productseries.ProductSeriesBreadcrumb"

=== added file 'lib/lp/registry/tests/test_adapters.py'
--- lib/lp/registry/tests/test_adapters.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_adapters.py	2011-03-08 14:48:38 +0000
@@ -0,0 +1,48 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for adapters."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.adapters import (
+    distroseries_to_distribution,
+    productseries_to_product,
+    )
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.product import IProduct
+from lp.testing import TestCaseWithFactory
+
+
+class TestAdapters(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_distroseries_to_distribution(self):
+        # distroseries_to_distribution() returns an IDistribution given an
+        # IDistroSeries.
+        distro_series = self.factory.makeDistroSeries()
+        distribution = distroseries_to_distribution(distro_series)
+        self.assertTrue(IDistribution.providedBy(distribution))
+
+    def test_distroseries_to_distribution_adapter(self):
+        # distroseries_to_distribution() is registered as an adapter from
+        # IDistroSeries to IDistribution.
+        distro_series = self.factory.makeDistroSeries()
+        distribution = IDistribution(distro_series)
+        self.assertTrue(IDistribution.providedBy(distribution))
+
+    def test_productseries_to_product(self):
+        # productseries_to_product() returns an IProduct given an
+        # IProductSeries.
+        product_series = self.factory.makeProductSeries()
+        product = productseries_to_product(product_series)
+        self.assertTrue(IProduct.providedBy(product))
+
+    def test_productseries_to_product_adapter(self):
+        # productseries_to_product() is registered as an adapter from
+        # IProductSeries to IProduct.
+        product_series = self.factory.makeProductSeries()
+        product = IProduct(product_series)
+        self.assertTrue(IProduct.providedBy(product))

=== added file 'lib/lp/registry/tests/test_distroseries_vocabularies.py'
--- lib/lp/registry/tests/test_distroseries_vocabularies.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_distroseries_vocabularies.py	2011-03-08 14:48:38 +0000
@@ -0,0 +1,191 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for distroseries vocabularies in `lp.registry.vocabularies`."""
+
+__metaclass__ = type
+
+from datetime import (
+    datetime,
+    timedelta,
+    )
+
+from pytz import utc
+from testtools.matchers import Equals
+from zope.component import getUtility
+from zope.schema.interfaces import (
+    IVocabulary,
+    IVocabularyFactory,
+    IVocabularyTokenized,
+    )
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.database.sqlbase import flush_database_caches
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.interfaces.distroseries import IDistroSeriesSet
+from lp.registry.vocabularies import DistroSeriesDerivationVocabularyFactory
+from lp.testing import (
+    StormStatementRecorder,
+    TestCaseWithFactory,
+    )
+from lp.testing.matchers import HasQueryCount
+
+
+class TestDistroSeriesDerivationVocabularyFactory(TestCaseWithFactory):
+    """Tests for `DistroSeriesDerivationVocabularyFactory`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestDistroSeriesDerivationVocabularyFactory, self).setUp()
+        self.vocabulary_factory = DistroSeriesDerivationVocabularyFactory
+        self.all_distroseries = getUtility(IDistroSeriesSet).search()
+
+    def test_registration(self):
+        # DistroSeriesDerivationVocabularyFactory is registered as a named
+        # utility for IVocabularyFactory.
+        self.assertEqual(
+            getUtility(IVocabularyFactory, name="DistroSeriesDerivation"),
+            DistroSeriesDerivationVocabularyFactory)
+
+    def test_interfaces(self):
+        # DistroSeriesDerivationVocabularyFactory instances provide
+        # IVocabulary and IVocabularyTokenized.
+        distribution = self.factory.makeDistribution()
+        vocabulary = self.vocabulary_factory(distribution)
+        self.assertProvides(vocabulary, IVocabulary)
+        self.assertProvides(vocabulary, IVocabularyTokenized)
+
+    def test_distribution_without_series(self):
+        # Given a distribution without any series, the vocabulary factory
+        # returns a vocabulary for all distroseries in all distributions.
+        distribution = self.factory.makeDistribution()
+        vocabulary = self.vocabulary_factory(distribution)
+        expected_distroseries = set(self.all_distroseries)
+        observed_distroseries = set(term.value for term in vocabulary)
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_distribution_with_non_derived_series(self):
+        # Given a distribution with series, none of which are derived, the
+        # vocabulary factory returns a vocabulary for all distroseries in all
+        # distributions *except* the given distribution.
+        distroseries = self.factory.makeDistroSeries()
+        vocabulary = self.vocabulary_factory(distroseries.distribution)
+        expected_distroseries = (
+            set(self.all_distroseries) - set(distroseries.distribution))
+        observed_distroseries = set(term.value for term in vocabulary)
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_distribution_with_derived_series(self):
+        # Given a distribution with series, one or more of which are derived,
+        # the vocabulary factory returns a vocabulary for all distroseries of
+        # the distribution from which the derived series have been derived.
+        parent_distroseries = self.factory.makeDistroSeries()
+        distroseries = self.factory.makeDistroSeries(
+            parent_series=parent_distroseries)
+        vocabulary = self.vocabulary_factory(distroseries.distribution)
+        expected_distroseries = set(parent_distroseries.distribution)
+        observed_distroseries = set(term.value for term in vocabulary)
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_distribution_with_derived_series_of_self(self):
+        # Given a distribution with series derived from other of its series
+        # (which shouldn't happen), the vocabulary factory returns a
+        # vocabulary for all distroseries in all distributions *except* the
+        # given distribution.
+        parent_distroseries = self.factory.makeDistroSeries()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=parent_distroseries.distribution,
+            parent_series=parent_distroseries)
+        vocabulary = self.vocabulary_factory(distroseries.distribution)
+        expected_distroseries = (
+            set(self.all_distroseries) - set(distroseries.distribution))
+        observed_distroseries = set(term.value for term in vocabulary)
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_distroseries(self):
+        # Given a distroseries, the vocabulary factory returns the vocabulary
+        # the same as for its distribution.
+        distroseries = self.factory.makeDistroSeries()
+        vocabulary = self.vocabulary_factory(distroseries)
+        expected_distroseries = (
+            set(self.all_distroseries) - set(distroseries.distribution))
+        observed_distroseries = set(term.value for term in vocabulary)
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_ordering(self):
+        # The vocabulary is sorted by distribution display name then by the
+        # date the distroseries was created, newest first.
+        now = datetime.now(utc)
+        two_days_ago = now - timedelta(2)
+        six_days_ago = now - timedelta(7)
+
+        aaa = self.factory.makeDistribution(displayname="aaa")
+        aaa_series_older = self.factory.makeDistroSeries(
+            name="aaa-series-older", distribution=aaa)
+        removeSecurityProxy(aaa_series_older).date_created = six_days_ago
+        aaa_series_newer = self.factory.makeDistroSeries(
+            name="aaa-series-newer", distribution=aaa)
+        removeSecurityProxy(aaa_series_newer).date_created = two_days_ago
+
+        bbb = self.factory.makeDistribution(displayname="bbb")
+        bbb_series_older = self.factory.makeDistroSeries(
+            name="bbb-series-older", distribution=bbb)
+        removeSecurityProxy(bbb_series_older).date_created = six_days_ago
+        bbb_series_newer = self.factory.makeDistroSeries(
+            name="bbb-series-newer", distribution=bbb)
+        removeSecurityProxy(bbb_series_newer).date_created = two_days_ago
+
+        ccc = self.factory.makeDistribution(displayname="ccc")
+
+        vocabulary = self.vocabulary_factory(ccc)
+        expected_distroseries = [
+            aaa_series_newer, aaa_series_older,
+            bbb_series_newer, bbb_series_older]
+        observed_distroseries = list(term.value for term in vocabulary)
+        # observed_distroseries will contain distroseries from the sample
+        # data, so we must only look at the set of distroseries we have
+        # created.
+        observed_distroseries = [
+            series for series in observed_distroseries
+            if series in expected_distroseries]
+        self.assertEqual(expected_distroseries, observed_distroseries)
+
+    def test_queries_for_distribution_without_series(self):
+        for index in range(10):
+            self.factory.makeDistroSeries()
+        distribution = self.factory.makeDistribution()
+        flush_database_caches()
+        # Getting terms issues two queries: one for the distribution's
+        # serieses and a second for all serieses.
+        with StormStatementRecorder() as recorder:
+            self.vocabulary_factory(distribution).terms
+            self.assertThat(recorder, HasQueryCount(Equals(2)))
+
+    def test_queries_for_distribution_with_non_derived_series(self):
+        for index in range(10):
+            self.factory.makeDistroSeries()
+        distribution = self.factory.makeDistribution()
+        self.factory.makeDistroSeries(distribution=distribution)
+        flush_database_caches()
+        # Getting terms issues two queries: one for the distribution's
+        # serieses, a second to search for parent serieses (of which there are
+        # none) and a third for all serieses.
+        with StormStatementRecorder() as recorder:
+            self.vocabulary_factory(distribution).terms
+            self.assertThat(recorder, HasQueryCount(Equals(3)))
+
+    def test_queries_for_distribution_with_derived_series(self):
+        for index in range(10):
+            self.factory.makeDistroSeries()
+        distribution = self.factory.makeDistribution()
+        parent_distroseries = self.factory.makeDistroSeries()
+        self.factory.makeDistroSeries(
+            parent_series=parent_distroseries,
+            distribution=distribution)
+        flush_database_caches()
+        # Getting terms issues two queries: one for the distribution's
+        # serieses and a second to find parent serieses.
+        with StormStatementRecorder() as recorder:
+            self.vocabulary_factory(distribution).terms
+            self.assertThat(recorder, HasQueryCount(Equals(2)))

=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py	2011-03-07 02:32:57 +0000
+++ lib/lp/registry/vocabularies.py	2011-03-08 14:48:38 +0000
@@ -31,6 +31,7 @@
     'DistributionOrProductOrProjectGroupVocabulary',
     'DistributionOrProductVocabulary',
     'DistributionVocabulary',
+    'DistroSeriesDerivationVocabularyFactory',
     'DistroSeriesVocabulary',
     'FeaturedProjectVocabulary',
     'FilteredDistroSeriesVocabulary',
@@ -38,28 +39,30 @@
     'KarmaCategoryVocabulary',
     'MilestoneVocabulary',
     'NonMergedPeopleAndTeamsVocabulary',
+    'person_team_participations_vocabulary_factory',
     'PersonAccountToMergeVocabulary',
     'PersonActiveMembershipVocabulary',
     'ProductReleaseVocabulary',
     'ProductSeriesVocabulary',
     'ProductVocabulary',
+    'project_products_vocabulary_factory',
     'ProjectGroupVocabulary',
     'SourcePackageNameVocabulary',
+    'UserTeamsParticipationPlusSelfVocabulary',
     'UserTeamsParticipationVocabulary',
-    'UserTeamsParticipationPlusSelfVocabulary',
     'ValidPersonOrClosedTeamVocabulary',
     'ValidPersonOrTeamVocabulary',
     'ValidPersonVocabulary',
     'ValidTeamMemberVocabulary',
     'ValidTeamOwnerVocabulary',
     'ValidTeamVocabulary',
-    'person_team_participations_vocabulary_factory',
-    'project_products_vocabulary_factory',
     ]
 
 
+from datetime import datetime
 from operator import attrgetter
 
+from pytz import utc
 from sqlobject import (
     AND,
     CONTAINSSTRING,
@@ -77,7 +80,10 @@
     )
 from zope.component import getUtility
 from zope.interface import implements
-from zope.schema.interfaces import IVocabularyTokenized
+from zope.schema.interfaces import (
+    IVocabulary,
+    IVocabularyTokenized,
+    )
 from zope.schema.vocabulary import (
     SimpleTerm,
     SimpleVocabulary,
@@ -1432,7 +1438,8 @@
         for series in sorted(series, key=attrgetter('sortkey')):
             yield self.toTerm(series)
 
-    def toTerm(self, obj):
+    @staticmethod
+    def toTerm(obj):
         """See `IVocabulary`."""
         # NB: We use '/' as the separator because '-' is valid in
         # a distribution.name
@@ -1474,6 +1481,119 @@
         return objs
 
 
+class DistroSeriesDerivationVocabularyFactory:
+    """A vocabulary source for series to derive from.
+
+    Once a distribution has a series that has derived from a series in another
+    distribution, all other derived series must also derive from a series in
+    the same distribution.
+
+    A distribution can have non-derived series. Any of these can be changed to
+    derived at a later date, but as soon as this happens, the above rule
+    applies.
+
+    It is permissible for a distribution to have both derived and non-derived
+    series at the same time.
+    """
+
+    implements(IVocabulary, IVocabularyTokenized)
+
+    def __init__(self, context):
+        """See `IVocabularyFactory.__call__`."""
+        self.context = context
+
+    def find_serieses(self, *where):
+        """Return a `tuple` of `DistroSeries` matching the given criteria.
+
+        The serieses are returned in order, and the related `Distribution`s
+        are preloaded at the same time.
+        """
+        query = IStore(DistroSeries).find(
+            (DistroSeries, Distribution),
+            DistroSeries.distribution == Distribution.id,
+            *where)
+        query = query.order_by(
+            Distribution.displayname,
+            Desc(DistroSeries.date_created))
+        return tuple(
+            series for (series, distribution) in query)
+
+    @cachedproperty
+    def terms(self):
+        """Terms for the series the context can derive from, in order.
+
+        The order is the same as for `DistroSeriesVocabulary`.
+        """
+        distribution = IDistribution(self.context)
+        context_serieses = set(distribution)
+        if len(context_serieses) == 0:
+            # Derive from any series.
+            serieses = self.find_serieses()
+        else:
+            # Derive only from series in the same distribution as other
+            # derived series in this distribution.
+            serieses = self.find_serieses(
+                DistroSeries.distribution != distribution,
+                DistroSeries.id.is_in(
+                    removeSecurityProxy(series).parent_seriesID
+                    for series in context_serieses))
+            if len(serieses) == 0:
+                # Derive from any series, except those in this distribution.
+                serieses = self.find_serieses(
+                    DistroSeries.distribution != distribution)
+        return tuple(
+            DistroSeriesVocabulary.toTerm(series)
+            for series in serieses)
+
+    @cachedproperty
+    def terms_by_value(self):
+        """Mapping of terms by value."""
+        return dict((term.value, term) for term in self.terms)
+
+    @cachedproperty
+    def terms_by_token(self):
+        """Mapping of terms by token."""
+        return dict((term.token, term) for term in self.terms)
+
+    def __iter__(self):
+        """Returns an iterator over the terms in the vocabulary.
+
+        See `IIterableVocabulary`.
+        """
+        return iter(self.terms)
+
+    def __len__(self):
+        """The number of terms.
+
+        See `IIterableVocabulary`.
+        """
+        return len(self.terms)
+
+    def getTerm(self, value):
+        """Return the `ITerm` object for the term 'value'.
+
+        See `IBaseVocabulary`.
+        """
+        try:
+            return self.terms_by_value[value]
+        except KeyError:
+            raise LookupError(value)
+
+    def __contains__(self, value):
+        """Return whether the value is available in this source.
+
+        See `ISource`.
+        """
+        return (value in self.terms_by_value)
+
+    def getTermByToken(self, token):
+        """See `IVocabularyTokenized`."""
+        try:
+            return self.terms_by_token[token]
+        except KeyError:
+            raise LookupError(token)
+
+
 class PillarVocabularyBase(NamedSQLObjectHugeVocabulary):
     """Active `IPillar` objects vocabulary."""
     displayname = 'Needs to be overridden'

=== modified file 'lib/lp/registry/vocabularies.zcml'
--- lib/lp/registry/vocabularies.zcml	2010-11-30 22:55:58 +0000
+++ lib/lp/registry/vocabularies.zcml	2011-03-08 14:48:38 +0000
@@ -30,6 +30,18 @@
 
 
   <securedutility
+      name="DistroSeriesDerivation"
+      component=".vocabularies.DistroSeriesDerivationVocabularyFactory"
+      provides="zope.schema.interfaces.IVocabularyFactory">
+    <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
+  </securedutility>
+
+  <class class=".vocabularies.DistroSeriesDerivationVocabularyFactory">
+    <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
+  </class>
+
+
+  <securedutility
     name="FeaturedProject"
     component="lp.registry.vocabularies.FeaturedProjectVocabulary"
     provides="zope.schema.interfaces.IVocabularyFactory"

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-03-06 23:15:05 +0000
+++ lib/lp/testing/factory.py	2011-03-08 14:48:38 +0000
@@ -3935,6 +3935,10 @@
         else:
             return attr
 
+    def __dir__(self):
+        """Let interactive users know what can be created."""
+        return dir(self._factory)
+
 
 def remove_security_proxy_and_shout_at_engineer(obj):
     """Remove an object's security proxy and print a warning.

=== modified file 'lib/lp/testing/tests/test_factory.py'
--- lib/lp/testing/tests/test_factory.py	2010-12-02 16:13:51 +0000
+++ lib/lp/testing/tests/test_factory.py	2011-03-08 14:48:38 +0000
@@ -611,6 +611,14 @@
             sequence='2000-1234', cvestate=CveStatus.DEPRECATED)
         self.assertEqual(CveStatus.DEPRECATED, cve.status)
 
+    # dir() support.
+    def test_dir(self):
+        # LaunchpadObjectFactory supports dir() even though all of its
+        # attributes are pseudo-attributes.
+        self.assertEqual(
+            dir(self.factory._factory),
+            dir(self.factory))
+
 
 class TestFactoryWithLibrarian(TestCaseWithFactory):
 


Follow ups