Merge lp:~cjwatson/launchpad/snap-series into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18027
Proposed branch: lp:~cjwatson/launchpad/snap-series
Merge into: lp:launchpad
Diff against target: 1041 lines (+877/-5)
12 files modified
lib/lp/app/browser/launchpad.py (+3/-1)
lib/lp/security.py (+12/-0)
lib/lp/snappy/browser/configure.zcml (+12/-1)
lib/lp/snappy/browser/snappyseries.py (+19/-0)
lib/lp/snappy/configure.zcml (+35/-0)
lib/lp/snappy/interfaces/snappyseries.py (+192/-0)
lib/lp/snappy/interfaces/webservice.py (+7/-1)
lib/lp/snappy/model/snappyseries.py (+184/-0)
lib/lp/snappy/tests/test_snappyseries.py (+279/-0)
lib/lp/snappy/vocabularies.py (+81/-1)
lib/lp/snappy/vocabularies.zcml (+34/-1)
lib/lp/testing/factory.py (+19/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-series
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+292516@code.launchpad.net

Commit message

Add SnapSeries and SnapDistroSeries models.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2016-04-11 06:38:48 +0000
+++ lib/lp/app/browser/launchpad.py 2016-05-09 13:24:49 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Browser code for the launchpad application."""4"""Browser code for the launchpad application."""
@@ -158,6 +158,7 @@
158from lp.services.worlddata.interfaces.country import ICountrySet158from lp.services.worlddata.interfaces.country import ICountrySet
159from lp.services.worlddata.interfaces.language import ILanguageSet159from lp.services.worlddata.interfaces.language import ILanguageSet
160from lp.snappy.interfaces.snap import ISnapSet160from lp.snappy.interfaces.snap import ISnapSet
161from lp.snappy.interfaces.snappyseries import ISnappySeriesSet
161from lp.soyuz.interfaces.archive import IArchiveSet162from lp.soyuz.interfaces.archive import IArchiveSet
162from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet163from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
163from lp.soyuz.interfaces.livefs import ILiveFSSet164from lp.soyuz.interfaces.livefs import ILiveFSSet
@@ -803,6 +804,7 @@
803 'projects': IProductSet,804 'projects': IProductSet,
804 'projectgroups': IProjectGroupSet,805 'projectgroups': IProjectGroupSet,
805 '+snaps': ISnapSet,806 '+snaps': ISnapSet,
807 '+snappy-series': ISnappySeriesSet,
806 'sourcepackagenames': ISourcePackageNameSet,808 'sourcepackagenames': ISourcePackageNameSet,
807 'specs': ISpecificationSet,809 'specs': ISpecificationSet,
808 'sprints': ISprintSet,810 'sprints': ISprintSet,
809811
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2016-04-12 10:50:30 +0000
+++ lib/lp/security.py 2016-05-09 13:24:49 +0000
@@ -194,6 +194,10 @@
194 )194 )
195from lp.snappy.interfaces.snap import ISnap195from lp.snappy.interfaces.snap import ISnap
196from lp.snappy.interfaces.snapbuild import ISnapBuild196from lp.snappy.interfaces.snapbuild import ISnapBuild
197from lp.snappy.interfaces.snappyseries import (
198 ISnappySeries,
199 ISnappySeriesSet,
200 )
197from lp.soyuz.interfaces.archive import IArchive201from lp.soyuz.interfaces.archive import IArchive
198from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken202from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
199from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet203from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3191,3 +3195,11 @@
31913195
3192class AdminSnapBuild(AdminByBuilddAdmin):3196class AdminSnapBuild(AdminByBuilddAdmin):
3193 usedfor = ISnapBuild3197 usedfor = ISnapBuild
3198
3199
3200class EditSnappySeries(EditByRegistryExpertsOrAdmins):
3201 usedfor = ISnappySeries
3202
3203
3204class EditSnappySeriesSet(EditByRegistryExpertsOrAdmins):
3205 usedfor = ISnappySeriesSet
31943206
=== 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-05-09 13:24:49 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2015 Canonical Ltd. This software is licensed under the1<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -117,6 +117,17 @@
117 for="lp.snappy.interfaces.snapbuild.ISnapBuild"117 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
118 factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"118 factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
119 permission="zope.Public" />119 permission="zope.Public" />
120 <browser:url
121 for="lp.snappy.interfaces.snappyseries.ISnappySeries"
122 path_expression="name"
123 parent_utility="lp.snappy.interfaces.snappyseries.ISnappySeriesSet" />
124 <browser:url
125 for="lp.snappy.interfaces.snappyseries.ISnappySeriesSet"
126 path_expression="string:+snappy-series"
127 parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" />
128 <browser:navigation
129 module="lp.snappy.browser.snappyseries"
130 classes="SnappySeriesSetNavigation" />
120131
121 <browser:page132 <browser:page
122 for="*"133 for="*"
123134
=== added file 'lib/lp/snappy/browser/snappyseries.py'
--- lib/lp/snappy/browser/snappyseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/snappyseries.py 2016-05-09 13:24:49 +0000
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""SnappySeries views."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'SnappySeriesSetNavigation',
11 ]
12
13from lp.services.webapp import GetitemNavigation
14from lp.snappy.interfaces.snappyseries import ISnappySeriesSet
15
16
17class SnappySeriesSetNavigation(GetitemNavigation):
18 """Navigation methods for `ISnappySeriesSet`."""
19 usedfor = ISnappySeriesSet
020
=== 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-05-09 13:24:49 +0000
@@ -82,6 +82,41 @@
82 <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />82 <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />
83 </class>83 </class>
8484
85 <!-- SnappySeries -->
86 <class class="lp.snappy.model.snappyseries.SnappySeries">
87 <allow
88 interface="lp.snappy.interfaces.snappyseries.ISnappySeriesView
89 lp.snappy.interfaces.snappyseries.ISnappySeriesEditableAttributes" />
90 <require
91 permission="launchpad.Edit"
92 set_schema="lp.snappy.interfaces.snappyseries.ISnappySeriesEditableAttributes" />
93 </class>
94
95 <!-- SnappyDistroSeries -->
96 <class class="lp.snappy.model.snappyseries.SnappyDistroSeries">
97 <allow
98 interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeries" />
99 </class>
100
101 <!-- SnappySeriesSet -->
102 <securedutility
103 class="lp.snappy.model.snappyseries.SnappySeriesSet"
104 provides="lp.snappy.interfaces.snappyseries.ISnappySeriesSet">
105 <allow
106 interface="lp.snappy.interfaces.snappyseries.ISnappySeriesSet" />
107 <require
108 permission="launchpad.Edit"
109 interface="lp.snappy.interfaces.snappyseries.ISnappySeriesSetEdit" />
110 </securedutility>
111
112 <!-- SnappyDistroSeriesSet -->
113 <securedutility
114 class="lp.snappy.model.snappyseries.SnappyDistroSeriesSet"
115 provides="lp.snappy.interfaces.snappyseries.ISnappyDistroSeriesSet">
116 <allow
117 interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeriesSet" />
118 </securedutility>
119
85 <webservice:register module="lp.snappy.interfaces.webservice" />120 <webservice:register module="lp.snappy.interfaces.webservice" />
86121
87</configure>122</configure>
88123
=== added file 'lib/lp/snappy/interfaces/snappyseries.py'
--- lib/lp/snappy/interfaces/snappyseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snappyseries.py 2016-05-09 13:24:49 +0000
@@ -0,0 +1,192 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snappy series interfaces."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'ISnappyDistroSeries',
11 'ISnappyDistroSeriesSet',
12 'ISnappySeries',
13 'ISnappySeriesSet',
14 'NoSuchSnappySeries',
15 ]
16
17from lazr.restful.declarations import (
18 call_with,
19 collection_default_content,
20 export_as_webservice_collection,
21 export_as_webservice_entry,
22 export_factory_operation,
23 export_read_operation,
24 exported,
25 operation_for_version,
26 operation_parameters,
27 operation_returns_collection_of,
28 operation_returns_entry,
29 REQUEST_USER,
30 )
31from lazr.restful.fields import Reference
32from zope.component import getUtility
33from zope.interface import Interface
34from zope.schema import (
35 Choice,
36 Datetime,
37 Int,
38 List,
39 TextLine,
40 )
41
42from lp import _
43from lp.app.errors import NameLookupFailed
44from lp.app.validators.name import name_validator
45from lp.registry.interfaces.distroseries import IDistroSeries
46from lp.registry.interfaces.series import SeriesStatus
47from lp.services.fields import (
48 ContentNameField,
49 PublicPersonChoice,
50 Title,
51 )
52
53
54class NoSuchSnappySeries(NameLookupFailed):
55 """The requested `SnappySeries` does not exist."""
56
57 _message_prefix = "No such snappy series"
58
59
60class SnappySeriesNameField(ContentNameField):
61 """Ensure that `ISnappySeries` has unique names."""
62
63 errormessage = _("%s is already in use by another series.")
64
65 @property
66 def _content_iface(self):
67 """See `UniqueField`."""
68 return ISnappySeries
69
70 def _getByName(self, name):
71 """See `ContentNameField`."""
72 try:
73 return getUtility(ISnappySeriesSet).getByName(name)
74 except NoSuchSnappySeries:
75 return None
76
77
78class ISnappySeriesView(Interface):
79 """`ISnappySeries` attributes that anyone can view."""
80
81 id = Int(title=_("ID"), required=True, readonly=True)
82
83 date_created = exported(Datetime(
84 title=_("Date created"), required=True, readonly=True))
85
86 registrant = exported(PublicPersonChoice(
87 title=_("Registrant"), required=True, readonly=True,
88 vocabulary="ValidPersonOrTeam",
89 description=_("The person who registered this snap package.")))
90
91
92class ISnappySeriesEditableAttributes(Interface):
93 """`ISnappySeries` attributes that can be edited.
94
95 Anyone can view these attributes, but they need launchpad.Edit to change.
96 """
97
98 name = exported(SnappySeriesNameField(
99 title=_("Name"), required=True, readonly=False,
100 constraint=name_validator))
101
102 display_name = exported(TextLine(
103 title=_("Display name"), required=True, readonly=False))
104
105 title = Title(title=_("Title"), required=True, readonly=True)
106
107 status = exported(Choice(
108 title=_("Status"), required=True, vocabulary=SeriesStatus))
109
110 usable_distro_series = exported(List(
111 title=_("Usable distro series"),
112 description=_(
113 "The distro series that can be used for this snappy series."),
114 value_type=Reference(schema=IDistroSeries),
115 required=True, readonly=False))
116
117
118class ISnappySeries(ISnappySeriesView, ISnappySeriesEditableAttributes):
119 """A series for snap packages in the store."""
120
121 # XXX cjwatson 2016-04-13 bug=760849: "beta" is a lie to get WADL
122 # generation working. Individual attributes must set their version to
123 # "devel".
124 export_as_webservice_entry(plural_name="snappy_serieses", as_of="beta")
125
126
127class ISnappyDistroSeries(Interface):
128 """A snappy/distro series link."""
129
130 snappy_series = Reference(
131 ISnappySeries, title=_("Snappy series"), readonly=True)
132 distro_series = Reference(
133 IDistroSeries, title=_("Distro series"), readonly=True)
134
135 title = Title(title=_("Title"), required=True, readonly=True)
136
137
138class ISnappySeriesSetEdit(Interface):
139 """`ISnappySeriesSet` methods that require launchpad.Edit permission."""
140
141 @call_with(registrant=REQUEST_USER)
142 @export_factory_operation(
143 ISnappySeries, ["name", "display_name", "status"])
144 @operation_for_version("devel")
145 def new(registrant, name, display_name, status, date_created=None):
146 """Create an `ISnappySeries`."""
147
148
149class ISnappySeriesSet(ISnappySeriesSetEdit):
150 """Interface representing the set of snappy series."""
151
152 export_as_webservice_collection(ISnappySeries)
153
154 def __iter__():
155 """Iterate over `ISnappySeries`."""
156
157 def __getitem__(name):
158 """Return the `ISnappySeries` with this name."""
159
160 @operation_parameters(
161 name=TextLine(title=_("Snappy series name"), required=True))
162 @operation_returns_entry(ISnappySeries)
163 @export_read_operation()
164 @operation_for_version("devel")
165 def getByName(name):
166 """Return the `ISnappySeries` with this name.
167
168 :raises NoSuchSnappySeries: if no snappy series exists with this name.
169 """
170
171 @operation_parameters(
172 distro_series=Reference(
173 IDistroSeries, title=_("Distro series"), required=True))
174 @operation_returns_collection_of(ISnappySeries)
175 @export_read_operation()
176 @operation_for_version("devel")
177 def getByDistroSeries(distro_series):
178 """Return all `ISnappySeries` usable with this `IDistroSeries`."""
179
180 @collection_default_content()
181 def getAll():
182 """Return all `ISnappySeries`."""
183
184
185class ISnappyDistroSeriesSet(Interface):
186 """Interface representing the set of snappy/distro series links."""
187
188 def getByDistroSeries(distro_series):
189 """Return all `SnappyDistroSeries` for this `IDistroSeries`."""
190
191 def getByBothSeries(snappy_series, distro_series):
192 """Return a `SnappyDistroSeries` for this pair of series, or None."""
0193
=== 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-05-09 13:24:49 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""All the interfaces that are exposed through the webservice.4"""All the interfaces that are exposed through the webservice.
@@ -12,6 +12,8 @@
12__all__ = [12__all__ = [
13 'ISnap',13 'ISnap',
14 'ISnapBuild',14 'ISnapBuild',
15 'ISnappySeries',
16 'ISnappySeriesSet',
15 'ISnapSet',17 'ISnapSet',
16 ]18 ]
1719
@@ -29,6 +31,10 @@
29 ISnapBuild,31 ISnapBuild,
30 ISnapFile,32 ISnapFile,
31 )33 )
34from lp.snappy.interfaces.snappyseries import (
35 ISnappySeries,
36 ISnappySeriesSet,
37 )
3238
3339
34# ISnapFile40# ISnapFile
3541
=== added file 'lib/lp/snappy/model/snappyseries.py'
--- lib/lp/snappy/model/snappyseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snappyseries.py 2016-05-09 13:24:49 +0000
@@ -0,0 +1,184 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snappy series."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'SnappyDistroSeries',
11 'SnappySeries',
12 ]
13
14import pytz
15from storm.locals import (
16 DateTime,
17 Desc,
18 Int,
19 Reference,
20 Store,
21 Storm,
22 Unicode,
23 )
24from zope.interface import implementer
25
26from lp.registry.interfaces.series import SeriesStatus
27from lp.registry.model.distroseries import DistroSeries
28from lp.services.database.constants import DEFAULT
29from lp.services.database.enumcol import EnumCol
30from lp.services.database.interfaces import (
31 IMasterStore,
32 IStore,
33 )
34from lp.snappy.interfaces.snappyseries import (
35 ISnappyDistroSeries,
36 ISnappyDistroSeriesSet,
37 ISnappySeries,
38 ISnappySeriesSet,
39 NoSuchSnappySeries,
40 )
41
42
43@implementer(ISnappySeries)
44class SnappySeries(Storm):
45 """See `ISnappySeries`."""
46
47 __storm_table__ = 'SnappySeries'
48
49 id = Int(primary=True)
50
51 date_created = DateTime(
52 name='date_created', tzinfo=pytz.UTC, allow_none=False)
53
54 registrant_id = Int(name='registrant', allow_none=False)
55 registrant = Reference(registrant_id, 'Person.id')
56
57 name = Unicode(name='name', allow_none=False)
58
59 display_name = Unicode(name='display_name', allow_none=False)
60
61 status = EnumCol(enum=SeriesStatus, notNull=True)
62
63 def __init__(self, registrant, name, display_name, status,
64 date_created=DEFAULT):
65 super(SnappySeries, self).__init__()
66 self.registrant = registrant
67 self.name = name
68 self.display_name = display_name
69 self.status = status
70 self.date_created = date_created
71
72 @property
73 def title(self):
74 return self.display_name
75
76 @property
77 def usable_distro_series(self):
78 rows = IStore(DistroSeries).find(
79 DistroSeries,
80 SnappyDistroSeries.snappy_series == self,
81 SnappyDistroSeries.distro_series_id == DistroSeries.id)
82 return rows.order_by(DistroSeries.id)
83
84 @usable_distro_series.setter
85 def usable_distro_series(self, value):
86 enablements = dict(Store.of(self).find(
87 (DistroSeries, SnappyDistroSeries),
88 SnappyDistroSeries.snappy_series == self,
89 SnappyDistroSeries.distro_series_id == DistroSeries.id))
90 for distro_series in enablements:
91 if distro_series not in value:
92 Store.of(self).remove(enablements[distro_series])
93 for distro_series in value:
94 if distro_series not in enablements:
95 link = SnappyDistroSeries(self, distro_series)
96 Store.of(self).add(link)
97
98
99@implementer(ISnappyDistroSeries)
100class SnappyDistroSeries(Storm):
101 """Link table between `SnappySeries` and `DistroSeries`."""
102
103 __storm_table__ = 'SnappyDistroSeries'
104 __storm_primary__ = ('snappy_series_id', 'distro_series_id')
105
106 snappy_series_id = Int(name='snappy_series', allow_none=False)
107 snappy_series = Reference(snappy_series_id, 'SnappySeries.id')
108
109 distro_series_id = Int(name='distro_series', allow_none=False)
110 distro_series = Reference(distro_series_id, 'DistroSeries.id')
111
112 def __init__(self, snappy_series, distro_series):
113 super(SnappyDistroSeries, self).__init__()
114 self.snappy_series = snappy_series
115 self.distro_series = distro_series
116
117 @property
118 def title(self):
119 return "%s, for %s" % (
120 self.distro_series.fullseriesname, self.snappy_series.title)
121
122
123@implementer(ISnappySeriesSet)
124class SnappySeriesSet:
125 """See `ISnappySeriesSet`."""
126
127 def new(self, registrant, name, display_name, status,
128 date_created=DEFAULT):
129 """See `ISnappySeriesSet`."""
130 store = IMasterStore(SnappySeries)
131 snappy_series = SnappySeries(
132 registrant, name, display_name, status, date_created=date_created)
133 store.add(snappy_series)
134 return snappy_series
135
136 def __iter__(self):
137 """See `ISnappySeriesSet`."""
138 return iter(self.getAll())
139
140 def __getitem__(self, name):
141 """See `ISnappySeriesSet`."""
142 return self.getByName(name)
143
144 def getByName(self, name):
145 """See `ISnappySeriesSet`."""
146 snappy_series = IStore(SnappySeries).find(
147 SnappySeries, SnappySeries.name == name).one()
148 if snappy_series is None:
149 raise NoSuchSnappySeries(name)
150 return snappy_series
151
152 def getByDistroSeries(self, distro_series):
153 """See `ISnappySeriesSet`."""
154 rows = IStore(SnappySeries).find(
155 SnappySeries,
156 SnappyDistroSeries.snappy_series_id == SnappySeries.id,
157 SnappyDistroSeries.distro_series == distro_series)
158 return rows.order_by(Desc(SnappySeries.name))
159
160 def getAll(self):
161 """See `ISnappySeriesSet`."""
162 return IStore(SnappySeries).find(SnappySeries).order_by(
163 SnappySeries.name)
164
165
166@implementer(ISnappyDistroSeriesSet)
167class SnappyDistroSeriesSet:
168 """See `ISnappyDistroSeriesSet`."""
169
170 def getByDistroSeries(self, distro_series):
171 """See `ISnappyDistroSeriesSet`."""
172 store = IStore(SnappyDistroSeries)
173 rows = store.using(SnappyDistroSeries, SnappySeries).find(
174 SnappyDistroSeries,
175 SnappyDistroSeries.snappy_series_id == SnappySeries.id,
176 SnappyDistroSeries.distro_series == distro_series)
177 return rows.order_by(Desc(SnappySeries.name))
178
179 def getByBothSeries(self, snappy_series, distro_series):
180 """See `ISnappyDistroSeriesSet`."""
181 return IStore(SnappyDistroSeries).find(
182 SnappyDistroSeries,
183 SnappyDistroSeries.snappy_series == snappy_series,
184 SnappyDistroSeries.distro_series == distro_series).one()
0185
=== added file 'lib/lp/snappy/tests/test_snappyseries.py'
--- lib/lp/snappy/tests/test_snappyseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snappyseries.py 2016-05-09 13:24:49 +0000
@@ -0,0 +1,279 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test snappy series."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from testtools.matchers import (
11 MatchesSetwise,
12 MatchesStructure,
13 )
14from zope.component import getUtility
15
16from lp.services.features.testing import FeatureFixture
17from lp.services.webapp.interfaces import OAuthPermission
18from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
19from lp.snappy.interfaces.snappyseries import (
20 ISnappyDistroSeriesSet,
21 ISnappySeries,
22 ISnappySeriesSet,
23 NoSuchSnappySeries,
24 )
25from lp.testing import (
26 admin_logged_in,
27 api_url,
28 logout,
29 person_logged_in,
30 TestCaseWithFactory,
31 )
32from lp.testing.layers import (
33 DatabaseFunctionalLayer,
34 ZopelessDatabaseLayer,
35 )
36from lp.testing.pages import webservice_for_person
37
38
39class TestSnappySeries(TestCaseWithFactory):
40
41 layer = ZopelessDatabaseLayer
42
43 def setUp(self):
44 super(TestSnappySeries, self).setUp()
45 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
46
47 def test_implements_interface(self):
48 # SnappySeries implements ISnappySeries.
49 snappy_series = self.factory.makeSnappySeries()
50 self.assertProvides(snappy_series, ISnappySeries)
51
52 def test_new_no_usable_distro_series(self):
53 snappy_series = self.factory.makeSnappySeries()
54 self.assertContentEqual([], snappy_series.usable_distro_series)
55
56 def test_set_usable_distro_series(self):
57 dses = [self.factory.makeDistroSeries() for _ in range(3)]
58 snappy_series = self.factory.makeSnappySeries(
59 usable_distro_series=[dses[0]])
60 self.assertContentEqual([dses[0]], snappy_series.usable_distro_series)
61 snappy_series.usable_distro_series = dses
62 self.assertContentEqual(dses, snappy_series.usable_distro_series)
63 snappy_series.usable_distro_series = []
64 self.assertContentEqual([], snappy_series.usable_distro_series)
65
66
67class TestSnappySeriesSet(TestCaseWithFactory):
68
69 layer = ZopelessDatabaseLayer
70
71 def setUp(self):
72 super(TestSnappySeriesSet, self).setUp()
73 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
74
75 def test_getByName(self):
76 snappy_series = self.factory.makeSnappySeries(name="foo")
77 self.factory.makeSnappySeries()
78 snappy_series_set = getUtility(ISnappySeriesSet)
79 self.assertEqual(snappy_series, snappy_series_set.getByName("foo"))
80 self.assertRaises(
81 NoSuchSnappySeries, snappy_series_set.getByName, "bar")
82
83 def test_getByDistroSeries(self):
84 dses = [self.factory.makeDistroSeries() for _ in range(3)]
85 snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)]
86 snappy_serieses[0].usable_distro_series = dses
87 snappy_serieses[1].usable_distro_series = [dses[0], dses[1]]
88 snappy_serieses[2].usable_distro_series = [dses[1], dses[2]]
89 snappy_series_set = getUtility(ISnappySeriesSet)
90 self.assertContentEqual(
91 [snappy_serieses[0], snappy_serieses[1]],
92 snappy_series_set.getByDistroSeries(dses[0]))
93 self.assertContentEqual(
94 snappy_serieses, snappy_series_set.getByDistroSeries(dses[1]))
95 self.assertContentEqual(
96 [snappy_serieses[0], snappy_serieses[2]],
97 snappy_series_set.getByDistroSeries(dses[2]))
98
99 def test_getAll(self):
100 snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)]
101 self.assertContentEqual(
102 snappy_serieses, getUtility(ISnappySeriesSet).getAll())
103
104
105class TestSnappySeriesWebservice(TestCaseWithFactory):
106
107 layer = DatabaseFunctionalLayer
108
109 def setUp(self):
110 super(TestSnappySeriesWebservice, self).setUp()
111 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
112
113 def test_new_unpriv(self):
114 # An unprivileged user cannot create a SnappySeries.
115 person = self.factory.makePerson()
116 webservice = webservice_for_person(
117 person, permission=OAuthPermission.WRITE_PUBLIC)
118 webservice.default_api_version = "devel"
119 response = webservice.named_post(
120 "/+snappy-series", "new",
121 name="dummy", display_name="dummy", status="Experimental")
122 self.assertEqual(401, response.status)
123
124 def test_new(self):
125 # A registry expert can create a SnappySeries.
126 person = self.factory.makeRegistryExpert()
127 webservice = webservice_for_person(
128 person, permission=OAuthPermission.WRITE_PUBLIC)
129 webservice.default_api_version = "devel"
130 logout()
131 response = webservice.named_post(
132 "/+snappy-series", "new",
133 name="dummy", display_name="Dummy", status="Experimental")
134 self.assertEqual(201, response.status)
135 snappy_series = webservice.get(
136 response.getHeader("Location")).jsonBody()
137 with person_logged_in(person):
138 self.assertEqual(
139 webservice.getAbsoluteUrl(api_url(person)),
140 snappy_series["registrant_link"])
141 self.assertEqual("dummy", snappy_series["name"])
142 self.assertEqual("Dummy", snappy_series["display_name"])
143 self.assertEqual("Experimental", snappy_series["status"])
144
145 def test_new_duplicate_name(self):
146 # An attempt to create a SnappySeries with a duplicate name is
147 # rejected.
148 person = self.factory.makeRegistryExpert()
149 webservice = webservice_for_person(
150 person, permission=OAuthPermission.WRITE_PUBLIC)
151 webservice.default_api_version = "devel"
152 logout()
153 response = webservice.named_post(
154 "/+snappy-series", "new",
155 name="dummy", display_name="Dummy", status="Experimental")
156 self.assertEqual(201, response.status)
157 response = webservice.named_post(
158 "/+snappy-series", "new",
159 name="dummy", display_name="Dummy", status="Experimental")
160 self.assertEqual(400, response.status)
161 self.assertEqual(
162 "name: dummy is already in use by another series.", response.body)
163
164 def test_getByName(self):
165 # lp.snappy_serieses.getByName returns a matching SnappySeries.
166 person = self.factory.makePerson()
167 webservice = webservice_for_person(
168 person, permission=OAuthPermission.READ_PUBLIC)
169 webservice.default_api_version = "devel"
170 with admin_logged_in():
171 self.factory.makeSnappySeries(name="dummy")
172 response = webservice.named_get(
173 "/+snappy-series", "getByName", name="dummy")
174 self.assertEqual(200, response.status)
175 self.assertEqual("dummy", response.jsonBody()["name"])
176
177 def test_getByName_missing(self):
178 # lp.snappy_serieses.getByName returns 404 for a non-existent
179 # SnappySeries.
180 person = self.factory.makePerson()
181 webservice = webservice_for_person(
182 person, permission=OAuthPermission.READ_PUBLIC)
183 webservice.default_api_version = "devel"
184 logout()
185 response = webservice.named_get(
186 "/+snappy-series", "getByName", name="nonexistent")
187 self.assertEqual(404, response.status)
188 self.assertEqual(
189 "No such snappy series: 'nonexistent'.", response.body)
190
191 def test_getByDistroSeries(self):
192 # lp.snappy_serieses.getByDistroSeries returns a collection of
193 # matching SnappySeries.
194 person = self.factory.makePerson()
195 webservice = webservice_for_person(
196 person, permission=OAuthPermission.READ_PUBLIC)
197 webservice.default_api_version = "devel"
198 with admin_logged_in():
199 dses = [self.factory.makeDistroSeries() for _ in range(3)]
200 ds_urls = [api_url(ds) for ds in dses]
201 snappy_serieses = [
202 self.factory.makeSnappySeries(name="ss-%d" % i)
203 for i in range(3)]
204 snappy_serieses[0].usable_distro_series = dses
205 snappy_serieses[1].usable_distro_series = [dses[0], dses[1]]
206 snappy_serieses[2].usable_distro_series = [dses[1], dses[2]]
207 for ds_url, expected_snappy_series_names in (
208 (ds_urls[0], ["ss-0", "ss-1"]),
209 (ds_urls[1], ["ss-0", "ss-1", "ss-2"]),
210 (ds_urls[2], ["ss-0", "ss-2"])):
211 response = webservice.named_get(
212 "/+snappy-series", "getByDistroSeries", distro_series=ds_url)
213 self.assertEqual(200, response.status)
214 self.assertContentEqual(
215 expected_snappy_series_names,
216 [entry["name"] for entry in response.jsonBody()["entries"]])
217
218 def test_collection(self):
219 # lp.snappy_serieses is a collection of all SnappySeries.
220 person = self.factory.makePerson()
221 webservice = webservice_for_person(
222 person, permission=OAuthPermission.READ_PUBLIC)
223 webservice.default_api_version = "devel"
224 with admin_logged_in():
225 for i in range(3):
226 self.factory.makeSnappySeries(name="ss-%d" % i)
227 response = webservice.get("/+snappy-series")
228 self.assertEqual(200, response.status)
229 self.assertContentEqual(
230 ["ss-0", "ss-1", "ss-2"],
231 [entry["name"] for entry in response.jsonBody()["entries"]])
232
233
234class TestSnappyDistroSeriesSet(TestCaseWithFactory):
235
236 layer = ZopelessDatabaseLayer
237
238 def setUp(self):
239 super(TestSnappyDistroSeriesSet, self).setUp()
240 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
241
242 def test_getByDistroSeries(self):
243 dses = [self.factory.makeDistroSeries() for _ in range(3)]
244 snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)]
245 snappy_serieses[0].usable_distro_series = dses
246 snappy_serieses[1].usable_distro_series = [dses[0], dses[1]]
247 snappy_serieses[2].usable_distro_series = [dses[1], dses[2]]
248 sds_set = getUtility(ISnappyDistroSeriesSet)
249 self.assertThat(
250 sds_set.getByDistroSeries(dses[0]),
251 MatchesSetwise(*(
252 MatchesStructure.byEquality(
253 snappy_series=ss, distro_series=dses[0])
254 for ss in (snappy_serieses[0], snappy_serieses[1]))))
255 self.assertThat(
256 sds_set.getByDistroSeries(dses[1]),
257 MatchesSetwise(*(
258 MatchesStructure.byEquality(
259 snappy_series=ss, distro_series=dses[1])
260 for ss in snappy_serieses)))
261 self.assertThat(
262 sds_set.getByDistroSeries(dses[2]),
263 MatchesSetwise(*(
264 MatchesStructure.byEquality(
265 snappy_series=ss, distro_series=dses[2])
266 for ss in (snappy_serieses[0], snappy_serieses[2]))))
267
268 def test_getByBothSeries(self):
269 dses = [self.factory.makeDistroSeries() for _ in range(2)]
270 snappy_serieses = [self.factory.makeSnappySeries() for _ in range(2)]
271 snappy_serieses[0].usable_distro_series = [dses[0]]
272 sds_set = getUtility(ISnappyDistroSeriesSet)
273 self.assertThat(
274 sds_set.getByBothSeries(snappy_serieses[0], dses[0]),
275 MatchesStructure.byEquality(
276 snappy_series=snappy_serieses[0], distro_series=dses[0]))
277 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1]))
278 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0]))
279 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1]))
0280
=== 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-05-09 13:24:49 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the GNU1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the GNU
2# Affero General Public License version 3 (see the file LICENSE).2# Affero General Public License version 3 (see the file LICENSE).
33
4"""Snappy vocabularies."""4"""Snappy vocabularies."""
@@ -7,11 +7,21 @@
77
8__all__ = [8__all__ = [
9 'SnapDistroArchSeriesVocabulary',9 'SnapDistroArchSeriesVocabulary',
10 'SnappySeriesVocabulary',
10 ]11 ]
1112
13from storm.locals import Desc
12from zope.schema.vocabulary import SimpleTerm14from zope.schema.vocabulary import SimpleTerm
1315
16from lp.registry.model.distribution import Distribution
17from lp.registry.model.distroseries import DistroSeries
18from lp.registry.model.series import ACTIVE_STATUSES
19from lp.services.database.interfaces import IStore
14from lp.services.webapp.vocabulary import StormVocabularyBase20from lp.services.webapp.vocabulary import StormVocabularyBase
21from lp.snappy.model.snappyseries import (
22 SnappyDistroSeries,
23 SnappySeries,
24 )
15from lp.soyuz.model.distroarchseries import DistroArchSeries25from lp.soyuz.model.distroarchseries import DistroArchSeries
1626
1727
@@ -29,3 +39,73 @@
2939
30 def __len__(self):40 def __len__(self):
31 return len(self.context.getAllowedArchitectures())41 return len(self.context.getAllowedArchitectures())
42
43
44class SnappySeriesVocabulary(StormVocabularyBase):
45 """A vocabulary for searching snappy series."""
46
47 _table = SnappySeries
48 _clauses = [SnappySeries.status.is_in(ACTIVE_STATUSES)]
49 _order_by = Desc(SnappySeries.date_created)
50
51
52class SnappyDistroSeriesVocabulary(StormVocabularyBase):
53 """A vocabulary for searching snappy/distro series combinations."""
54
55 _table = SnappyDistroSeries
56 _clauses = [
57 SnappyDistroSeries.snappy_series_id == SnappySeries.id,
58 SnappyDistroSeries.distro_series_id == DistroSeries.id,
59 DistroSeries.distributionID == Distribution.id,
60 ]
61
62 @property
63 def _entries(self):
64 tables = [SnappyDistroSeries, SnappySeries, DistroSeries, Distribution]
65 entries = IStore(self._table).using(*tables).find(
66 self._table, *self._clauses)
67 return entries.order_by(
68 Distribution.display_name, Desc(DistroSeries.date_created),
69 Desc(SnappySeries.date_created))
70
71 def toTerm(self, obj):
72 """See `IVocabulary`."""
73 token = "%s/%s/%s" % (
74 obj.distro_series.distribution.name, obj.distro_series.name,
75 obj.snappy_series.name)
76 return SimpleTerm(obj, token, obj.title)
77
78 def __contains__(self, value):
79 """See `IVocabulary`."""
80 return value in self._entries
81
82 def getTerm(self, value):
83 """See `IVocabulary`."""
84 if value not in self:
85 raise LookupError(value)
86 return self.toTerm(value)
87
88 def getTermByToken(self, token):
89 """See `IVocabularyTokenized`."""
90 try:
91 distribution_name, distro_series_name, snappy_series_name = (
92 token.split("/", 2))
93 except ValueError:
94 raise LookupError(token)
95 entry = IStore(self._table).find(
96 self._table,
97 Distribution.name == distribution_name,
98 DistroSeries.name == distro_series_name,
99 SnappySeries.name == snappy_series_name,
100 *self._clauses).one()
101 if entry is None:
102 raise LookupError(token)
103 return self.toTerm(entry)
104
105
106class BuildableSnappyDistroSeriesVocabulary(SnappyDistroSeriesVocabulary):
107 """A vocabulary for searching active snappy/distro series combinations."""
108
109 _clauses = SnappyDistroSeriesVocabulary._clauses + [
110 SnappySeries.status.is_in(ACTIVE_STATUSES),
111 ]
32112
=== 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-05-09 13:24:49 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2015 Canonical Ltd. This software is licensed under the1<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -15,4 +15,37 @@
15 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />15 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
16 </class>16 </class>
1717
18 <securedutility
19 name="SnappySeries"
20 component="lp.snappy.vocabularies.SnappySeriesVocabulary"
21 provides="zope.schema.interfaces.IVocabularyFactory">
22 <allow interface="zope.schema.interfaces.IVocabularyFactory" />
23 </securedutility>
24
25 <class class="lp.snappy.vocabularies.SnappySeriesVocabulary">
26 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
27 </class>
28
29 <securedutility
30 name="SnappyDistroSeries"
31 component="lp.snappy.vocabularies.SnappyDistroSeriesVocabulary"
32 provides="zope.schema.interfaces.IVocabularyFactory">
33 <allow interface="zope.schema.interfaces.IVocabularyFactory" />
34 </securedutility>
35
36 <class class="lp.snappy.vocabularies.SnappyDistroSeriesVocabulary">
37 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
38 </class>
39
40 <securedutility
41 name="BuildableSnappyDistroSeries"
42 component="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary"
43 provides="zope.schema.interfaces.IVocabularyFactory">
44 <allow interface="zope.schema.interfaces.IVocabularyFactory" />
45 </securedutility>
46
47 <class class="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary">
48 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
49 </class>
50
18</configure>51</configure>
1952
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2016-04-28 02:25:46 +0000
+++ lib/lp/testing/factory.py 2016-05-09 13:24:49 +0000
@@ -275,6 +275,7 @@
275from lp.services.worlddata.interfaces.language import ILanguageSet275from lp.services.worlddata.interfaces.language import ILanguageSet
276from lp.snappy.interfaces.snap import ISnapSet276from lp.snappy.interfaces.snap import ISnapSet
277from lp.snappy.interfaces.snapbuild import ISnapBuildSet277from lp.snappy.interfaces.snapbuild import ISnapBuildSet
278from lp.snappy.interfaces.snappyseries import ISnappySeriesSet
278from lp.snappy.model.snapbuild import SnapFile279from lp.snappy.model.snapbuild import SnapFile
279from lp.soyuz.adapters.overrides import SourceOverride280from lp.soyuz.adapters.overrides import SourceOverride
280from lp.soyuz.adapters.packagelocation import PackageLocation281from lp.soyuz.adapters.packagelocation import PackageLocation
@@ -4679,6 +4680,24 @@
4679 return ProxyFactory(4680 return ProxyFactory(
4680 SnapFile(snapbuild=snapbuild, libraryfile=libraryfile))4681 SnapFile(snapbuild=snapbuild, libraryfile=libraryfile))
46814682
4683 def makeSnappySeries(self, registrant=None, name=None, display_name=None,
4684 status=SeriesStatus.DEVELOPMENT,
4685 date_created=DEFAULT, usable_distro_series=None):
4686 """Make a new SnappySeries."""
4687 if registrant is None:
4688 registrant = self.makePerson()
4689 if name is None:
4690 name = self.getUniqueString(u"snappy-series-name")
4691 if display_name is None:
4692 display_name = SPACE.join(
4693 word.capitalize() for word in name.split('-'))
4694 snappy_series = getUtility(ISnappySeriesSet).new(
4695 registrant, name, display_name, status, date_created=date_created)
4696 if usable_distro_series is not None:
4697 snappy_series.usable_distro_series = usable_distro_series
4698 IStore(snappy_series).flush()
4699 return snappy_series
4700
46824701
4683# Some factory methods return simple Python types. We don't add4702# Some factory methods return simple Python types. We don't add
4684# security wrappers for them, as well as for objects created by4703# security wrappers for them, as well as for objects created by