launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20250
[Merge] lp:~cjwatson/launchpad/snap-series into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-series into lp:launchpad.
Commit message:
Add SnapSeries and SnapDistroSeries models.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1572605 in Launchpad itself: "Automatically upload snap builds to store"
https://bugs.launchpad.net/launchpad/+bug/1572605
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-series/+merge/292516
Add SnapSeries and SnapDistroSeries models. This is preparation for being able to upload snap builds to the store, for which Launchpad needs to have a basic idea of the store's view of series. SnapDistroSeries exists so that for a snap based on a given DistroSeries we can have some idea of which SnapSeries are legitimate targets.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-series into lp:launchpad.
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2015-08-06 00:46:39 +0000
+++ lib/lp/app/browser/launchpad.py 2016-04-21 13:21:13 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Browser code for the launchpad application."""
@@ -158,6 +158,7 @@
from lp.services.worlddata.interfaces.country import ICountrySet
from lp.services.worlddata.interfaces.language import ILanguageSet
from lp.snappy.interfaces.snap import ISnapSet
+from lp.snappy.interfaces.snapseries import ISnapSeriesSet
from lp.soyuz.interfaces.archive import IArchiveSet
from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
from lp.soyuz.interfaces.livefs import ILiveFSSet
@@ -803,6 +804,7 @@
'projects': IProductSet,
'projectgroups': IProjectGroupSet,
'+snaps': ISnapSet,
+ '+snap-series': ISnapSeriesSet,
'sourcepackagenames': ISourcePackageNameSet,
'specs': ISpecificationSet,
'sprints': ISprintSet,
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2016-04-12 10:50:30 +0000
+++ lib/lp/security.py 2016-04-21 13:21:13 +0000
@@ -194,6 +194,10 @@
)
from lp.snappy.interfaces.snap import ISnap
from lp.snappy.interfaces.snapbuild import ISnapBuild
+from lp.snappy.interfaces.snapseries import (
+ ISnapSeries,
+ ISnapSeriesSet,
+ )
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3191,3 +3195,11 @@
class AdminSnapBuild(AdminByBuilddAdmin):
usedfor = ISnapBuild
+
+
+class EditSnapSeries(EditByRegistryExpertsOrAdmins):
+ usedfor = ISnapSeries
+
+
+class EditSnapSeriesSet(EditByRegistryExpertsOrAdmins):
+ usedfor = ISnapSeriesSet
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2016-02-28 17:12:41 +0000
+++ lib/lp/snappy/browser/configure.zcml 2016-04-21 13:21:13 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -117,6 +117,19 @@
for="lp.snappy.interfaces.snapbuild.ISnapBuild"
factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
permission="zope.Public" />
+ <browser:url
+ for="lp.snappy.interfaces.snapseries.ISnapSeries"
+ path_expression="name"
+ parent_utility="lp.snappy.interfaces.snapseries.ISnapSeriesSet" />
+ <browser:url
+ for="lp.snappy.interfaces.snapseries.ISnapSeriesSet"
+ path_expression="string:+snap-series"
+ parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" />
+ <browser:navigation
+ module="lp.snappy.browser.snapseries"
+ classes="
+ SnapSeriesNavigation
+ SnapSeriesSetNavigation" />
<browser:page
for="*"
=== added file 'lib/lp/snappy/browser/snapseries.py'
--- lib/lp/snappy/browser/snapseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/snapseries.py 2016-04-21 13:21:13 +0000
@@ -0,0 +1,31 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""SnapSeries views."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'SnapSeriesNavigation',
+ 'SnapSeriesSetNavigation',
+ ]
+
+from lp.services.webapp import (
+ GetitemNavigation,
+ Navigation,
+ )
+from lp.snappy.interfaces.snapseries import (
+ ISnapSeries,
+ ISnapSeriesSet,
+ )
+
+
+class SnapSeriesSetNavigation(GetitemNavigation):
+ """Navigation methods for `ISnapSeriesSet`."""
+ usedfor = ISnapSeriesSet
+
+
+class SnapSeriesNavigation(Navigation):
+ """Navigation methods for `ISnapSeries`."""
+ usedfor = ISnapSeries
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2016-01-19 17:41:11 +0000
+++ lib/lp/snappy/configure.zcml 2016-04-21 13:21:13 +0000
@@ -82,6 +82,27 @@
<allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />
</class>
+ <!-- SnapSeries -->
+ <class class="lp.snappy.model.snapseries.SnapSeries">
+ <allow
+ interface="lp.snappy.interfaces.snapseries.ISnapSeriesView
+ lp.snappy.interfaces.snapseries.ISnapSeriesEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ set_schema="lp.snappy.interfaces.snapseries.ISnapSeriesEditableAttributes" />
+ </class>
+
+ <!-- SnapSeriesSet -->
+ <securedutility
+ class="lp.snappy.model.snapseries.SnapSeriesSet"
+ provides="lp.snappy.interfaces.snapseries.ISnapSeriesSet">
+ <allow
+ interface="lp.snappy.interfaces.snapseries.ISnapSeriesSet" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.snappy.interfaces.snapseries.ISnapSeriesSetEdit" />
+ </securedutility>
+
<webservice:register module="lp.snappy.interfaces.webservice" />
</configure>
=== added file 'lib/lp/snappy/interfaces/snapseries.py'
--- lib/lp/snappy/interfaces/snapseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snapseries.py 2016-04-21 13:21:13 +0000
@@ -0,0 +1,168 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap series interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ISnapSeries',
+ 'ISnapSeriesSet',
+ 'NoSuchSnapSeries',
+ ]
+
+from lazr.restful.declarations import (
+ call_with,
+ collection_default_content,
+ export_as_webservice_collection,
+ export_as_webservice_entry,
+ export_factory_operation,
+ export_read_operation,
+ exported,
+ operation_for_version,
+ operation_parameters,
+ operation_returns_collection_of,
+ operation_returns_entry,
+ REQUEST_USER,
+ )
+from lazr.restful.fields import Reference
+from zope.component import getUtility
+from zope.interface import Interface
+from zope.schema import (
+ Choice,
+ Datetime,
+ Int,
+ List,
+ TextLine,
+ )
+
+from lp import _
+from lp.app.errors import NameLookupFailed
+from lp.app.validators.name import name_validator
+from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.fields import (
+ ContentNameField,
+ PublicPersonChoice,
+ Title,
+ )
+
+
+class NoSuchSnapSeries(NameLookupFailed):
+ """The requested `SnapSeries` does not exist."""
+
+ _message_prefix = "No such snap series"
+
+
+class SnapSeriesNameField(ContentNameField):
+ """Ensure that `ISnapSeries` has unique names."""
+
+ errormessage = _("%s is already in use by another series.")
+
+ @property
+ def _content_iface(self):
+ """See `UniqueField`."""
+ return ISnapSeries
+
+ def _getByName(self, name):
+ """See `ContentNameField`."""
+ try:
+ return getUtility(ISnapSeriesSet).getByName(name)
+ except NoSuchSnapSeries:
+ return None
+
+
+class ISnapSeriesView(Interface):
+ """`ISnapSeries` attributes that require launchpad.View permission."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+ date_created = exported(Datetime(
+ title=_("Date created"), required=True, readonly=True))
+
+ registrant = exported(PublicPersonChoice(
+ title=_("Registrant"), required=True, readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person who registered this snap package.")))
+
+
+class ISnapSeriesEditableAttributes(Interface):
+ """`ISnapSeries` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ name = exported(SnapSeriesNameField(
+ title=_("Name"), required=True, readonly=False,
+ constraint=name_validator))
+
+ display_name = exported(TextLine(
+ title=_("Display name"), required=True, readonly=False))
+
+ title = Title(title=_("Title"), required=True, readonly=True)
+
+ status = exported(Choice(
+ title=_("Status"), required=True, vocabulary=SeriesStatus))
+
+ usable_distro_series = exported(List(
+ title=_("Usable distro series"),
+ description=_(
+ "The distro series that can be used for this snap series."),
+ value_type=Reference(schema=IDistroSeries),
+ required=True, readonly=False))
+
+
+class ISnapSeries(ISnapSeriesView, ISnapSeriesEditableAttributes):
+ """A series for snap packages in the store."""
+
+ # XXX cjwatson 2016-04-13 bug=760849: "beta" is a lie to get WADL
+ # generation working. Individual attributes must set their version to
+ # "devel".
+ export_as_webservice_entry(plural_name="snap_serieses", as_of="beta")
+
+
+class ISnapSeriesSetEdit(Interface):
+ """`ISnapSeriesSet` methods that require launchpad.Edit permission."""
+
+ @call_with(registrant=REQUEST_USER)
+ @export_factory_operation(ISnapSeries, ["name", "display_name", "status"])
+ @operation_for_version("devel")
+ def new(registrant, name, display_name, status, date_created=None):
+ """Create an `ISnapSeries`."""
+
+
+class ISnapSeriesSet(ISnapSeriesSetEdit):
+ """Interface representing the set of snap series."""
+
+ export_as_webservice_collection(ISnapSeries)
+
+ def __iter__():
+ """Iterate over `ISnapSeries`."""
+
+ def __getitem__(name):
+ """Return the `ISnapSeries` with this name."""
+
+ @operation_parameters(
+ name=TextLine(title=_("Snap series name"), required=True))
+ @operation_returns_entry(ISnapSeries)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getByName(name):
+ """Return the `ISnapSeries` with this name.
+
+ :raises NoSuchSnapSeries: if no snap series exists with this name.
+ """
+
+ @operation_parameters(
+ distro_series=Reference(
+ IDistroSeries, title=_("Distro series"), required=True))
+ @operation_returns_collection_of(ISnapSeries)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getByDistroSeries(distro_series):
+ """Return all `ISnapSeries` usable with this `IDistroSeries`."""
+
+ @collection_default_content()
+ def getAll():
+ """Return all `ISnapSeries`."""
=== modified file 'lib/lp/snappy/interfaces/webservice.py'
--- lib/lp/snappy/interfaces/webservice.py 2015-08-04 23:52:48 +0000
+++ lib/lp/snappy/interfaces/webservice.py 2016-04-21 13:21:13 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""All the interfaces that are exposed through the webservice.
@@ -12,6 +12,8 @@
__all__ = [
'ISnap',
'ISnapBuild',
+ 'ISnapSeries',
+ 'ISnapSeriesSet',
'ISnapSet',
]
@@ -29,6 +31,10 @@
ISnapBuild,
ISnapFile,
)
+from lp.snappy.interfaces.snapseries import (
+ ISnapSeries,
+ ISnapSeriesSet,
+ )
# ISnapFile
=== added file 'lib/lp/snappy/model/snapseries.py'
--- lib/lp/snappy/model/snapseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapseries.py 2016-04-21 13:21:13 +0000
@@ -0,0 +1,149 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap series."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'SnapSeries',
+ ]
+
+import pytz
+from storm.locals import (
+ DateTime,
+ Int,
+ Reference,
+ Store,
+ Storm,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.model.distroseries import DistroSeries
+from lp.services.database.constants import DEFAULT
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.snappy.interfaces.snapseries import (
+ ISnapSeries,
+ ISnapSeriesSet,
+ NoSuchSnapSeries,
+ )
+
+
+@implementer(ISnapSeries)
+class SnapSeries(Storm):
+ """See `ISnapSeries`."""
+
+ __storm_table__ = 'SnapSeries'
+
+ id = Int(primary=True)
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+
+ registrant_id = Int(name='registrant', allow_none=False)
+ registrant = Reference(registrant_id, 'Person.id')
+
+ name = Unicode(name='name', allow_none=False)
+
+ display_name = Unicode(name='display_name', allow_none=False)
+
+ status = EnumCol(enum=SeriesStatus, notNull=True)
+
+ def __init__(self, registrant, name, display_name, status,
+ date_created=DEFAULT):
+ super(SnapSeries, self).__init__()
+ self.registrant = registrant
+ self.name = name
+ self.display_name = display_name
+ self.status = status
+ self.date_created = date_created
+
+ @property
+ def title(self):
+ return self.display_name
+
+ @property
+ def usable_distro_series(self):
+ rows = IStore(DistroSeries).find(
+ DistroSeries,
+ SnapDistroSeries.snap_series == self,
+ SnapDistroSeries.distro_series_id == DistroSeries.id)
+ return rows.order_by(DistroSeries.id)
+
+ @usable_distro_series.setter
+ def usable_distro_series(self, value):
+ enablements = dict(Store.of(self).find(
+ (DistroSeries, SnapDistroSeries),
+ SnapDistroSeries.snap_series == self,
+ SnapDistroSeries.distro_series_id == DistroSeries.id))
+ for distro_series in enablements:
+ if distro_series not in value:
+ Store.of(self).remove(enablements[distro_series])
+ for distro_series in value:
+ if distro_series not in enablements:
+ link = SnapDistroSeries()
+ link.snap_series = self
+ link.distro_series = distro_series
+ Store.of(self).add(link)
+
+
+class SnapDistroSeries(Storm):
+ """Link table between `SnapSeries` and `DistroSeries`."""
+
+ __storm_table__ = 'SnapDistroSeries'
+ __storm_primary__ = ('snap_series_id', 'distro_series_id')
+
+ snap_series_id = Int(name='snap_series', allow_none=False)
+ snap_series = Reference(snap_series_id, 'SnapSeries.id')
+
+ distro_series_id = Int(name='distro_series', allow_none=False)
+ distro_series = Reference(distro_series_id, 'DistroSeries.id')
+
+
+@implementer(ISnapSeriesSet)
+class SnapSeriesSet:
+ """See `ISnapSeriesSet`."""
+
+ def new(self, registrant, name, display_name, status,
+ date_created=DEFAULT):
+ """See `ISnapSeriesSet`."""
+ store = IMasterStore(SnapSeries)
+ snap_series = SnapSeries(
+ registrant, name, display_name, status, date_created=date_created)
+ store.add(snap_series)
+ return snap_series
+
+ def __iter__(self):
+ """See `ISnapSeriesSet`."""
+ return iter(self.getAll())
+
+ def __getitem__(self, name):
+ """See `ISnapSeriesSet`."""
+ return self.getByName(name)
+
+ def getByName(self, name):
+ """See `ISnapSeriesSet`."""
+ snap_series = IStore(SnapSeries).find(
+ SnapSeries, SnapSeries.name == name).one()
+ if snap_series is None:
+ raise NoSuchSnapSeries(name)
+ return snap_series
+
+ def getByDistroSeries(self, distro_series):
+ """See `ISnapSeriesSet`."""
+ rows = IStore(SnapSeries).find(
+ SnapSeries,
+ SnapDistroSeries.snap_series_id == SnapSeries.id,
+ SnapDistroSeries.distro_series == distro_series)
+ return rows.order_by(SnapSeries.name)
+
+ def getAll(self):
+ """See `ISnapSeriesSet`."""
+ return IStore(SnapSeries).find(SnapSeries).order_by(SnapSeries.name)
=== added file 'lib/lp/snappy/tests/test_snapseries.py'
--- lib/lp/snappy/tests/test_snapseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapseries.py 2016-04-21 13:21:13 +0000
@@ -0,0 +1,221 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test snap series."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+from lp.services.features.testing import FeatureFixture
+from lp.services.webapp.interfaces import OAuthPermission
+from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
+from lp.snappy.interfaces.snapseries import (
+ ISnapSeries,
+ ISnapSeriesSet,
+ NoSuchSnapSeries,
+ )
+from lp.testing import (
+ admin_logged_in,
+ api_url,
+ logout,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ ZopelessDatabaseLayer,
+ )
+from lp.testing.pages import webservice_for_person
+
+
+class TestSnapSeries(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def setUp(self):
+ super(TestSnapSeries, self).setUp()
+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
+
+ def test_implements_interface(self):
+ # SnapSeries implements ISnapSeries.
+ snap_series = self.factory.makeSnapSeries()
+ self.assertProvides(snap_series, ISnapSeries)
+
+ def test_new_no_usable_distro_series(self):
+ snap_series = self.factory.makeSnapSeries()
+ self.assertContentEqual([], snap_series.usable_distro_series)
+
+ def test_set_usable_distro_series(self):
+ dses = [self.factory.makeDistroSeries() for _ in range(3)]
+ snap_series = self.factory.makeSnapSeries()
+ snap_series.usable_distro_series = [dses[0]]
+ self.assertContentEqual([dses[0]], snap_series.usable_distro_series)
+ snap_series.usable_distro_series = dses
+ self.assertContentEqual(dses, snap_series.usable_distro_series)
+ snap_series.usable_distro_series = []
+ self.assertContentEqual([], snap_series.usable_distro_series)
+
+
+class TestSnapSeriesSet(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def setUp(self):
+ super(TestSnapSeriesSet, self).setUp()
+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
+
+ def test_getByName(self):
+ snap_series = self.factory.makeSnapSeries(name="foo")
+ self.factory.makeSnapSeries()
+ snap_series_set = getUtility(ISnapSeriesSet)
+ self.assertEqual(snap_series, snap_series_set.getByName("foo"))
+ self.assertRaises(NoSuchSnapSeries, snap_series_set.getByName, "bar")
+
+ def test_getByDistroSeries(self):
+ dses = [self.factory.makeDistroSeries() for _ in range(3)]
+ snap_serieses = [self.factory.makeSnapSeries() for _ in range(3)]
+ snap_serieses[0].usable_distro_series = dses
+ snap_serieses[1].usable_distro_series = [dses[0], dses[1]]
+ snap_serieses[2].usable_distro_series = [dses[1], dses[2]]
+ snap_series_set = getUtility(ISnapSeriesSet)
+ self.assertContentEqual(
+ [snap_serieses[0], snap_serieses[1]],
+ snap_series_set.getByDistroSeries(dses[0]))
+ self.assertContentEqual(
+ snap_serieses, snap_series_set.getByDistroSeries(dses[1]))
+ self.assertContentEqual(
+ [snap_serieses[0], snap_serieses[2]],
+ snap_series_set.getByDistroSeries(dses[2]))
+
+ def test_getAll(self):
+ snap_serieses = [self.factory.makeSnapSeries() for _ in range(3)]
+ self.assertContentEqual(
+ snap_serieses, getUtility(ISnapSeriesSet).getAll())
+
+
+class TestSnapSeriesWebservice(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapSeriesWebservice, self).setUp()
+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
+
+ def test_new_unpriv(self):
+ # An unprivileged user cannot create a SnapSeries.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_post(
+ "/+snap-series", "new",
+ name="dummy", display_name="dummy", status="Experimental")
+ self.assertEqual(401, response.status)
+
+ def test_new(self):
+ # A registry expert can create a SnapSeries.
+ person = self.factory.makeRegistryExpert()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_post(
+ "/+snap-series", "new",
+ name="dummy", display_name="Dummy", status="Experimental")
+ self.assertEqual(201, response.status)
+ snap_series = webservice.get(response.getHeader("Location")).jsonBody()
+ with person_logged_in(person):
+ self.assertEqual(
+ webservice.getAbsoluteUrl(api_url(person)),
+ snap_series["registrant_link"])
+ self.assertEqual("dummy", snap_series["name"])
+ self.assertEqual("Dummy", snap_series["display_name"])
+ self.assertEqual("Experimental", snap_series["status"])
+
+ def test_new_duplicate_name(self):
+ # An attempt to create a SnapSeries with a duplicate name is rejected.
+ person = self.factory.makeRegistryExpert()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_post(
+ "/+snap-series", "new",
+ name="dummy", display_name="Dummy", status="Experimental")
+ self.assertEqual(201, response.status)
+ response = webservice.named_post(
+ "/+snap-series", "new",
+ name="dummy", display_name="Dummy", status="Experimental")
+ self.assertEqual(400, response.status)
+ self.assertEqual(
+ "name: dummy is already in use by another series.", response.body)
+
+ def test_getByName(self):
+ # lp.snap_series.getByName returns a matching SnapSeries.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ with admin_logged_in():
+ self.factory.makeSnapSeries(name="dummy")
+ response = webservice.named_get(
+ "/+snap-series", "getByName", name="dummy")
+ self.assertEqual(200, response.status)
+ self.assertEqual("dummy", response.jsonBody()["name"])
+
+ def test_getByName_missing(self):
+ # lp.snap_series.getByName returns 404 for a non-existent SnapSeries.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_get(
+ "/+snap-series", "getByName", name="nonexistent")
+ self.assertEqual(404, response.status)
+ self.assertEqual("No such snap series: 'nonexistent'.", response.body)
+
+ def test_getByDistroSeries(self):
+ # lp.snap_series.getByDistroSeries returns a collection of matching
+ # SnapSeries.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ with admin_logged_in():
+ dses = [self.factory.makeDistroSeries() for _ in range(3)]
+ ds_urls = [api_url(ds) for ds in dses]
+ snap_serieses = [
+ self.factory.makeSnapSeries(name="ss-%d" % i)
+ for i in range(3)]
+ snap_serieses[0].usable_distro_series = dses
+ snap_serieses[1].usable_distro_series = [dses[0], dses[1]]
+ snap_serieses[2].usable_distro_series = [dses[1], dses[2]]
+ for ds_url, expected_snap_series_names in (
+ (ds_urls[0], ["ss-0", "ss-1"]),
+ (ds_urls[1], ["ss-0", "ss-1", "ss-2"]),
+ (ds_urls[2], ["ss-0", "ss-2"])):
+ response = webservice.named_get(
+ "/+snap-series", "getByDistroSeries", distro_series=ds_url)
+ self.assertEqual(200, response.status)
+ self.assertContentEqual(
+ expected_snap_series_names,
+ [entry["name"] for entry in response.jsonBody()["entries"]])
+
+ def test_collection(self):
+ # lp.snap_series is a collection of all SnapSeries.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ with admin_logged_in():
+ for i in range(3):
+ self.factory.makeSnapSeries(name="ss-%d" % i)
+ response = webservice.get("/+snap-series")
+ self.assertEqual(200, response.status)
+ self.assertContentEqual(
+ ["ss-0", "ss-1", "ss-2"],
+ [entry["name"] for entry in response.jsonBody()["entries"]])
=== modified file 'lib/lp/snappy/vocabularies.py'
--- lib/lp/snappy/vocabularies.py 2015-09-18 14:14:34 +0000
+++ lib/lp/snappy/vocabularies.py 2016-04-21 13:21:13 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the GNU
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the GNU
# Affero General Public License version 3 (see the file LICENSE).
"""Snappy vocabularies."""
@@ -7,11 +7,14 @@
__all__ = [
'SnapDistroArchSeriesVocabulary',
+ 'SnapSeriesVocabulary',
]
from zope.schema.vocabulary import SimpleTerm
+from lp.registry.model.series import ACTIVE_STATUSES
from lp.services.webapp.vocabulary import StormVocabularyBase
+from lp.snappy.model.snapseries import SnapSeries
from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -29,3 +32,10 @@
def __len__(self):
return len(self.context.getAllowedArchitectures())
+
+
+class SnapSeriesVocabulary(StormVocabularyBase):
+ """A vocabulary for searching snap series."""
+
+ _table = SnapSeries
+ _clauses = [SnapSeries.status.is_in(ACTIVE_STATUSES)]
=== modified file 'lib/lp/snappy/vocabularies.zcml'
--- lib/lp/snappy/vocabularies.zcml 2015-09-18 13:32:09 +0000
+++ lib/lp/snappy/vocabularies.zcml 2016-04-21 13:21:13 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -15,4 +15,15 @@
<allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
</class>
+ <securedutility
+ name="SnapSeries"
+ component="lp.snappy.vocabularies.SnapSeriesVocabulary"
+ provides="zope.schema.interfaces.IVocabularyFactory">
+ <allow interface="zope.schema.interfaces.IVocabularyFactory" />
+ </securedutility>
+
+ <class class="lp.snappy.vocabularies.SnapSeriesVocabulary">
+ <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
+ </class>
+
</configure>
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2016-04-04 10:06:33 +0000
+++ lib/lp/testing/factory.py 2016-04-21 13:21:13 +0000
@@ -275,6 +275,7 @@
from lp.services.worlddata.interfaces.language import ILanguageSet
from lp.snappy.interfaces.snap import ISnapSet
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.snappy.interfaces.snapseries import ISnapSeriesSet
from lp.snappy.model.snapbuild import SnapFile
from lp.soyuz.adapters.overrides import SourceOverride
from lp.soyuz.adapters.packagelocation import PackageLocation
@@ -4672,6 +4673,21 @@
return ProxyFactory(
SnapFile(snapbuild=snapbuild, libraryfile=libraryfile))
+ def makeSnapSeries(self, registrant=None, name=None, display_name=None,
+ status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT):
+ """Make a new SnapSeries."""
+ if registrant is None:
+ registrant = self.makePerson()
+ if name is None:
+ name = self.getUniqueString(u"snap-series-name")
+ if display_name is None:
+ display_name = SPACE.join(
+ word.capitalize() for word in name.split('-'))
+ snap_series = getUtility(ISnapSeriesSet).new(
+ registrant, name, display_name, status, date_created=date_created)
+ IStore(snap_series).flush()
+ return snap_series
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
Follow ups