launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02896
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