Merge lp:~cjwatson/launchpad/snap-series into lp:launchpad
- snap-series
- Merge into devel
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 | ||||
Related bugs: |
|
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
1 | === modified file 'lib/lp/app/browser/launchpad.py' | |||
2 | --- lib/lp/app/browser/launchpad.py 2016-04-11 06:38:48 +0000 | |||
3 | +++ lib/lp/app/browser/launchpad.py 2016-05-09 13:24:49 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2014 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Browser code for the launchpad application.""" | 4 | """Browser code for the launchpad application.""" |
10 | @@ -158,6 +158,7 @@ | |||
11 | 158 | from lp.services.worlddata.interfaces.country import ICountrySet | 158 | from lp.services.worlddata.interfaces.country import ICountrySet |
12 | 159 | from lp.services.worlddata.interfaces.language import ILanguageSet | 159 | from lp.services.worlddata.interfaces.language import ILanguageSet |
13 | 160 | from lp.snappy.interfaces.snap import ISnapSet | 160 | from lp.snappy.interfaces.snap import ISnapSet |
14 | 161 | from lp.snappy.interfaces.snappyseries import ISnappySeriesSet | ||
15 | 161 | from lp.soyuz.interfaces.archive import IArchiveSet | 162 | from lp.soyuz.interfaces.archive import IArchiveSet |
16 | 162 | from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet | 163 | from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet |
17 | 163 | from lp.soyuz.interfaces.livefs import ILiveFSSet | 164 | from lp.soyuz.interfaces.livefs import ILiveFSSet |
18 | @@ -803,6 +804,7 @@ | |||
19 | 803 | 'projects': IProductSet, | 804 | 'projects': IProductSet, |
20 | 804 | 'projectgroups': IProjectGroupSet, | 805 | 'projectgroups': IProjectGroupSet, |
21 | 805 | '+snaps': ISnapSet, | 806 | '+snaps': ISnapSet, |
22 | 807 | '+snappy-series': ISnappySeriesSet, | ||
23 | 806 | 'sourcepackagenames': ISourcePackageNameSet, | 808 | 'sourcepackagenames': ISourcePackageNameSet, |
24 | 807 | 'specs': ISpecificationSet, | 809 | 'specs': ISpecificationSet, |
25 | 808 | 'sprints': ISprintSet, | 810 | 'sprints': ISprintSet, |
26 | 809 | 811 | ||
27 | === modified file 'lib/lp/security.py' | |||
28 | --- lib/lp/security.py 2016-04-12 10:50:30 +0000 | |||
29 | +++ lib/lp/security.py 2016-05-09 13:24:49 +0000 | |||
30 | @@ -194,6 +194,10 @@ | |||
31 | 194 | ) | 194 | ) |
32 | 195 | from lp.snappy.interfaces.snap import ISnap | 195 | from lp.snappy.interfaces.snap import ISnap |
33 | 196 | from lp.snappy.interfaces.snapbuild import ISnapBuild | 196 | from lp.snappy.interfaces.snapbuild import ISnapBuild |
34 | 197 | from lp.snappy.interfaces.snappyseries import ( | ||
35 | 198 | ISnappySeries, | ||
36 | 199 | ISnappySeriesSet, | ||
37 | 200 | ) | ||
38 | 197 | from lp.soyuz.interfaces.archive import IArchive | 201 | from lp.soyuz.interfaces.archive import IArchive |
39 | 198 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken | 202 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken |
40 | 199 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet | 203 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
41 | @@ -3191,3 +3195,11 @@ | |||
42 | 3191 | 3195 | ||
43 | 3192 | class AdminSnapBuild(AdminByBuilddAdmin): | 3196 | class AdminSnapBuild(AdminByBuilddAdmin): |
44 | 3193 | usedfor = ISnapBuild | 3197 | usedfor = ISnapBuild |
45 | 3198 | |||
46 | 3199 | |||
47 | 3200 | class EditSnappySeries(EditByRegistryExpertsOrAdmins): | ||
48 | 3201 | usedfor = ISnappySeries | ||
49 | 3202 | |||
50 | 3203 | |||
51 | 3204 | class EditSnappySeriesSet(EditByRegistryExpertsOrAdmins): | ||
52 | 3205 | usedfor = ISnappySeriesSet | ||
53 | 3194 | 3206 | ||
54 | === modified file 'lib/lp/snappy/browser/configure.zcml' | |||
55 | --- lib/lp/snappy/browser/configure.zcml 2016-02-28 17:12:41 +0000 | |||
56 | +++ lib/lp/snappy/browser/configure.zcml 2016-05-09 13:24:49 +0000 | |||
57 | @@ -1,4 +1,4 @@ | |||
59 | 1 | <!-- Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
60 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
61 | 3 | --> | 3 | --> |
62 | 4 | 4 | ||
63 | @@ -117,6 +117,17 @@ | |||
64 | 117 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" | 117 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
65 | 118 | factory="lp.services.webapp.breadcrumb.TitleBreadcrumb" | 118 | factory="lp.services.webapp.breadcrumb.TitleBreadcrumb" |
66 | 119 | permission="zope.Public" /> | 119 | permission="zope.Public" /> |
67 | 120 | <browser:url | ||
68 | 121 | for="lp.snappy.interfaces.snappyseries.ISnappySeries" | ||
69 | 122 | path_expression="name" | ||
70 | 123 | parent_utility="lp.snappy.interfaces.snappyseries.ISnappySeriesSet" /> | ||
71 | 124 | <browser:url | ||
72 | 125 | for="lp.snappy.interfaces.snappyseries.ISnappySeriesSet" | ||
73 | 126 | path_expression="string:+snappy-series" | ||
74 | 127 | parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" /> | ||
75 | 128 | <browser:navigation | ||
76 | 129 | module="lp.snappy.browser.snappyseries" | ||
77 | 130 | classes="SnappySeriesSetNavigation" /> | ||
78 | 120 | 131 | ||
79 | 121 | <browser:page | 132 | <browser:page |
80 | 122 | for="*" | 133 | for="*" |
81 | 123 | 134 | ||
82 | === added file 'lib/lp/snappy/browser/snappyseries.py' | |||
83 | --- lib/lp/snappy/browser/snappyseries.py 1970-01-01 00:00:00 +0000 | |||
84 | +++ lib/lp/snappy/browser/snappyseries.py 2016-05-09 13:24:49 +0000 | |||
85 | @@ -0,0 +1,19 @@ | |||
86 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
87 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
88 | 3 | |||
89 | 4 | """SnappySeries views.""" | ||
90 | 5 | |||
91 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
92 | 7 | |||
93 | 8 | __metaclass__ = type | ||
94 | 9 | __all__ = [ | ||
95 | 10 | 'SnappySeriesSetNavigation', | ||
96 | 11 | ] | ||
97 | 12 | |||
98 | 13 | from lp.services.webapp import GetitemNavigation | ||
99 | 14 | from lp.snappy.interfaces.snappyseries import ISnappySeriesSet | ||
100 | 15 | |||
101 | 16 | |||
102 | 17 | class SnappySeriesSetNavigation(GetitemNavigation): | ||
103 | 18 | """Navigation methods for `ISnappySeriesSet`.""" | ||
104 | 19 | usedfor = ISnappySeriesSet | ||
105 | 0 | 20 | ||
106 | === modified file 'lib/lp/snappy/configure.zcml' | |||
107 | --- lib/lp/snappy/configure.zcml 2016-01-19 17:41:11 +0000 | |||
108 | +++ lib/lp/snappy/configure.zcml 2016-05-09 13:24:49 +0000 | |||
109 | @@ -82,6 +82,41 @@ | |||
110 | 82 | <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" /> | 82 | <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" /> |
111 | 83 | </class> | 83 | </class> |
112 | 84 | 84 | ||
113 | 85 | <!-- SnappySeries --> | ||
114 | 86 | <class class="lp.snappy.model.snappyseries.SnappySeries"> | ||
115 | 87 | <allow | ||
116 | 88 | interface="lp.snappy.interfaces.snappyseries.ISnappySeriesView | ||
117 | 89 | lp.snappy.interfaces.snappyseries.ISnappySeriesEditableAttributes" /> | ||
118 | 90 | <require | ||
119 | 91 | permission="launchpad.Edit" | ||
120 | 92 | set_schema="lp.snappy.interfaces.snappyseries.ISnappySeriesEditableAttributes" /> | ||
121 | 93 | </class> | ||
122 | 94 | |||
123 | 95 | <!-- SnappyDistroSeries --> | ||
124 | 96 | <class class="lp.snappy.model.snappyseries.SnappyDistroSeries"> | ||
125 | 97 | <allow | ||
126 | 98 | interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeries" /> | ||
127 | 99 | </class> | ||
128 | 100 | |||
129 | 101 | <!-- SnappySeriesSet --> | ||
130 | 102 | <securedutility | ||
131 | 103 | class="lp.snappy.model.snappyseries.SnappySeriesSet" | ||
132 | 104 | provides="lp.snappy.interfaces.snappyseries.ISnappySeriesSet"> | ||
133 | 105 | <allow | ||
134 | 106 | interface="lp.snappy.interfaces.snappyseries.ISnappySeriesSet" /> | ||
135 | 107 | <require | ||
136 | 108 | permission="launchpad.Edit" | ||
137 | 109 | interface="lp.snappy.interfaces.snappyseries.ISnappySeriesSetEdit" /> | ||
138 | 110 | </securedutility> | ||
139 | 111 | |||
140 | 112 | <!-- SnappyDistroSeriesSet --> | ||
141 | 113 | <securedutility | ||
142 | 114 | class="lp.snappy.model.snappyseries.SnappyDistroSeriesSet" | ||
143 | 115 | provides="lp.snappy.interfaces.snappyseries.ISnappyDistroSeriesSet"> | ||
144 | 116 | <allow | ||
145 | 117 | interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeriesSet" /> | ||
146 | 118 | </securedutility> | ||
147 | 119 | |||
148 | 85 | <webservice:register module="lp.snappy.interfaces.webservice" /> | 120 | <webservice:register module="lp.snappy.interfaces.webservice" /> |
149 | 86 | 121 | ||
150 | 87 | </configure> | 122 | </configure> |
151 | 88 | 123 | ||
152 | === added file 'lib/lp/snappy/interfaces/snappyseries.py' | |||
153 | --- lib/lp/snappy/interfaces/snappyseries.py 1970-01-01 00:00:00 +0000 | |||
154 | +++ lib/lp/snappy/interfaces/snappyseries.py 2016-05-09 13:24:49 +0000 | |||
155 | @@ -0,0 +1,192 @@ | |||
156 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
157 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
158 | 3 | |||
159 | 4 | """Snappy series interfaces.""" | ||
160 | 5 | |||
161 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
162 | 7 | |||
163 | 8 | __metaclass__ = type | ||
164 | 9 | __all__ = [ | ||
165 | 10 | 'ISnappyDistroSeries', | ||
166 | 11 | 'ISnappyDistroSeriesSet', | ||
167 | 12 | 'ISnappySeries', | ||
168 | 13 | 'ISnappySeriesSet', | ||
169 | 14 | 'NoSuchSnappySeries', | ||
170 | 15 | ] | ||
171 | 16 | |||
172 | 17 | from lazr.restful.declarations import ( | ||
173 | 18 | call_with, | ||
174 | 19 | collection_default_content, | ||
175 | 20 | export_as_webservice_collection, | ||
176 | 21 | export_as_webservice_entry, | ||
177 | 22 | export_factory_operation, | ||
178 | 23 | export_read_operation, | ||
179 | 24 | exported, | ||
180 | 25 | operation_for_version, | ||
181 | 26 | operation_parameters, | ||
182 | 27 | operation_returns_collection_of, | ||
183 | 28 | operation_returns_entry, | ||
184 | 29 | REQUEST_USER, | ||
185 | 30 | ) | ||
186 | 31 | from lazr.restful.fields import Reference | ||
187 | 32 | from zope.component import getUtility | ||
188 | 33 | from zope.interface import Interface | ||
189 | 34 | from zope.schema import ( | ||
190 | 35 | Choice, | ||
191 | 36 | Datetime, | ||
192 | 37 | Int, | ||
193 | 38 | List, | ||
194 | 39 | TextLine, | ||
195 | 40 | ) | ||
196 | 41 | |||
197 | 42 | from lp import _ | ||
198 | 43 | from lp.app.errors import NameLookupFailed | ||
199 | 44 | from lp.app.validators.name import name_validator | ||
200 | 45 | from lp.registry.interfaces.distroseries import IDistroSeries | ||
201 | 46 | from lp.registry.interfaces.series import SeriesStatus | ||
202 | 47 | from lp.services.fields import ( | ||
203 | 48 | ContentNameField, | ||
204 | 49 | PublicPersonChoice, | ||
205 | 50 | Title, | ||
206 | 51 | ) | ||
207 | 52 | |||
208 | 53 | |||
209 | 54 | class NoSuchSnappySeries(NameLookupFailed): | ||
210 | 55 | """The requested `SnappySeries` does not exist.""" | ||
211 | 56 | |||
212 | 57 | _message_prefix = "No such snappy series" | ||
213 | 58 | |||
214 | 59 | |||
215 | 60 | class SnappySeriesNameField(ContentNameField): | ||
216 | 61 | """Ensure that `ISnappySeries` has unique names.""" | ||
217 | 62 | |||
218 | 63 | errormessage = _("%s is already in use by another series.") | ||
219 | 64 | |||
220 | 65 | @property | ||
221 | 66 | def _content_iface(self): | ||
222 | 67 | """See `UniqueField`.""" | ||
223 | 68 | return ISnappySeries | ||
224 | 69 | |||
225 | 70 | def _getByName(self, name): | ||
226 | 71 | """See `ContentNameField`.""" | ||
227 | 72 | try: | ||
228 | 73 | return getUtility(ISnappySeriesSet).getByName(name) | ||
229 | 74 | except NoSuchSnappySeries: | ||
230 | 75 | return None | ||
231 | 76 | |||
232 | 77 | |||
233 | 78 | class ISnappySeriesView(Interface): | ||
234 | 79 | """`ISnappySeries` attributes that anyone can view.""" | ||
235 | 80 | |||
236 | 81 | id = Int(title=_("ID"), required=True, readonly=True) | ||
237 | 82 | |||
238 | 83 | date_created = exported(Datetime( | ||
239 | 84 | title=_("Date created"), required=True, readonly=True)) | ||
240 | 85 | |||
241 | 86 | registrant = exported(PublicPersonChoice( | ||
242 | 87 | title=_("Registrant"), required=True, readonly=True, | ||
243 | 88 | vocabulary="ValidPersonOrTeam", | ||
244 | 89 | description=_("The person who registered this snap package."))) | ||
245 | 90 | |||
246 | 91 | |||
247 | 92 | class ISnappySeriesEditableAttributes(Interface): | ||
248 | 93 | """`ISnappySeries` attributes that can be edited. | ||
249 | 94 | |||
250 | 95 | Anyone can view these attributes, but they need launchpad.Edit to change. | ||
251 | 96 | """ | ||
252 | 97 | |||
253 | 98 | name = exported(SnappySeriesNameField( | ||
254 | 99 | title=_("Name"), required=True, readonly=False, | ||
255 | 100 | constraint=name_validator)) | ||
256 | 101 | |||
257 | 102 | display_name = exported(TextLine( | ||
258 | 103 | title=_("Display name"), required=True, readonly=False)) | ||
259 | 104 | |||
260 | 105 | title = Title(title=_("Title"), required=True, readonly=True) | ||
261 | 106 | |||
262 | 107 | status = exported(Choice( | ||
263 | 108 | title=_("Status"), required=True, vocabulary=SeriesStatus)) | ||
264 | 109 | |||
265 | 110 | usable_distro_series = exported(List( | ||
266 | 111 | title=_("Usable distro series"), | ||
267 | 112 | description=_( | ||
268 | 113 | "The distro series that can be used for this snappy series."), | ||
269 | 114 | value_type=Reference(schema=IDistroSeries), | ||
270 | 115 | required=True, readonly=False)) | ||
271 | 116 | |||
272 | 117 | |||
273 | 118 | class ISnappySeries(ISnappySeriesView, ISnappySeriesEditableAttributes): | ||
274 | 119 | """A series for snap packages in the store.""" | ||
275 | 120 | |||
276 | 121 | # XXX cjwatson 2016-04-13 bug=760849: "beta" is a lie to get WADL | ||
277 | 122 | # generation working. Individual attributes must set their version to | ||
278 | 123 | # "devel". | ||
279 | 124 | export_as_webservice_entry(plural_name="snappy_serieses", as_of="beta") | ||
280 | 125 | |||
281 | 126 | |||
282 | 127 | class ISnappyDistroSeries(Interface): | ||
283 | 128 | """A snappy/distro series link.""" | ||
284 | 129 | |||
285 | 130 | snappy_series = Reference( | ||
286 | 131 | ISnappySeries, title=_("Snappy series"), readonly=True) | ||
287 | 132 | distro_series = Reference( | ||
288 | 133 | IDistroSeries, title=_("Distro series"), readonly=True) | ||
289 | 134 | |||
290 | 135 | title = Title(title=_("Title"), required=True, readonly=True) | ||
291 | 136 | |||
292 | 137 | |||
293 | 138 | class ISnappySeriesSetEdit(Interface): | ||
294 | 139 | """`ISnappySeriesSet` methods that require launchpad.Edit permission.""" | ||
295 | 140 | |||
296 | 141 | @call_with(registrant=REQUEST_USER) | ||
297 | 142 | @export_factory_operation( | ||
298 | 143 | ISnappySeries, ["name", "display_name", "status"]) | ||
299 | 144 | @operation_for_version("devel") | ||
300 | 145 | def new(registrant, name, display_name, status, date_created=None): | ||
301 | 146 | """Create an `ISnappySeries`.""" | ||
302 | 147 | |||
303 | 148 | |||
304 | 149 | class ISnappySeriesSet(ISnappySeriesSetEdit): | ||
305 | 150 | """Interface representing the set of snappy series.""" | ||
306 | 151 | |||
307 | 152 | export_as_webservice_collection(ISnappySeries) | ||
308 | 153 | |||
309 | 154 | def __iter__(): | ||
310 | 155 | """Iterate over `ISnappySeries`.""" | ||
311 | 156 | |||
312 | 157 | def __getitem__(name): | ||
313 | 158 | """Return the `ISnappySeries` with this name.""" | ||
314 | 159 | |||
315 | 160 | @operation_parameters( | ||
316 | 161 | name=TextLine(title=_("Snappy series name"), required=True)) | ||
317 | 162 | @operation_returns_entry(ISnappySeries) | ||
318 | 163 | @export_read_operation() | ||
319 | 164 | @operation_for_version("devel") | ||
320 | 165 | def getByName(name): | ||
321 | 166 | """Return the `ISnappySeries` with this name. | ||
322 | 167 | |||
323 | 168 | :raises NoSuchSnappySeries: if no snappy series exists with this name. | ||
324 | 169 | """ | ||
325 | 170 | |||
326 | 171 | @operation_parameters( | ||
327 | 172 | distro_series=Reference( | ||
328 | 173 | IDistroSeries, title=_("Distro series"), required=True)) | ||
329 | 174 | @operation_returns_collection_of(ISnappySeries) | ||
330 | 175 | @export_read_operation() | ||
331 | 176 | @operation_for_version("devel") | ||
332 | 177 | def getByDistroSeries(distro_series): | ||
333 | 178 | """Return all `ISnappySeries` usable with this `IDistroSeries`.""" | ||
334 | 179 | |||
335 | 180 | @collection_default_content() | ||
336 | 181 | def getAll(): | ||
337 | 182 | """Return all `ISnappySeries`.""" | ||
338 | 183 | |||
339 | 184 | |||
340 | 185 | class ISnappyDistroSeriesSet(Interface): | ||
341 | 186 | """Interface representing the set of snappy/distro series links.""" | ||
342 | 187 | |||
343 | 188 | def getByDistroSeries(distro_series): | ||
344 | 189 | """Return all `SnappyDistroSeries` for this `IDistroSeries`.""" | ||
345 | 190 | |||
346 | 191 | def getByBothSeries(snappy_series, distro_series): | ||
347 | 192 | """Return a `SnappyDistroSeries` for this pair of series, or None.""" | ||
348 | 0 | 193 | ||
349 | === modified file 'lib/lp/snappy/interfaces/webservice.py' | |||
350 | --- lib/lp/snappy/interfaces/webservice.py 2015-08-04 23:52:48 +0000 | |||
351 | +++ lib/lp/snappy/interfaces/webservice.py 2016-05-09 13:24:49 +0000 | |||
352 | @@ -1,4 +1,4 @@ | |||
354 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
355 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
356 | 3 | 3 | ||
357 | 4 | """All the interfaces that are exposed through the webservice. | 4 | """All the interfaces that are exposed through the webservice. |
358 | @@ -12,6 +12,8 @@ | |||
359 | 12 | __all__ = [ | 12 | __all__ = [ |
360 | 13 | 'ISnap', | 13 | 'ISnap', |
361 | 14 | 'ISnapBuild', | 14 | 'ISnapBuild', |
362 | 15 | 'ISnappySeries', | ||
363 | 16 | 'ISnappySeriesSet', | ||
364 | 15 | 'ISnapSet', | 17 | 'ISnapSet', |
365 | 16 | ] | 18 | ] |
366 | 17 | 19 | ||
367 | @@ -29,6 +31,10 @@ | |||
368 | 29 | ISnapBuild, | 31 | ISnapBuild, |
369 | 30 | ISnapFile, | 32 | ISnapFile, |
370 | 31 | ) | 33 | ) |
371 | 34 | from lp.snappy.interfaces.snappyseries import ( | ||
372 | 35 | ISnappySeries, | ||
373 | 36 | ISnappySeriesSet, | ||
374 | 37 | ) | ||
375 | 32 | 38 | ||
376 | 33 | 39 | ||
377 | 34 | # ISnapFile | 40 | # ISnapFile |
378 | 35 | 41 | ||
379 | === added file 'lib/lp/snappy/model/snappyseries.py' | |||
380 | --- lib/lp/snappy/model/snappyseries.py 1970-01-01 00:00:00 +0000 | |||
381 | +++ lib/lp/snappy/model/snappyseries.py 2016-05-09 13:24:49 +0000 | |||
382 | @@ -0,0 +1,184 @@ | |||
383 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
384 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
385 | 3 | |||
386 | 4 | """Snappy series.""" | ||
387 | 5 | |||
388 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
389 | 7 | |||
390 | 8 | __metaclass__ = type | ||
391 | 9 | __all__ = [ | ||
392 | 10 | 'SnappyDistroSeries', | ||
393 | 11 | 'SnappySeries', | ||
394 | 12 | ] | ||
395 | 13 | |||
396 | 14 | import pytz | ||
397 | 15 | from storm.locals import ( | ||
398 | 16 | DateTime, | ||
399 | 17 | Desc, | ||
400 | 18 | Int, | ||
401 | 19 | Reference, | ||
402 | 20 | Store, | ||
403 | 21 | Storm, | ||
404 | 22 | Unicode, | ||
405 | 23 | ) | ||
406 | 24 | from zope.interface import implementer | ||
407 | 25 | |||
408 | 26 | from lp.registry.interfaces.series import SeriesStatus | ||
409 | 27 | from lp.registry.model.distroseries import DistroSeries | ||
410 | 28 | from lp.services.database.constants import DEFAULT | ||
411 | 29 | from lp.services.database.enumcol import EnumCol | ||
412 | 30 | from lp.services.database.interfaces import ( | ||
413 | 31 | IMasterStore, | ||
414 | 32 | IStore, | ||
415 | 33 | ) | ||
416 | 34 | from lp.snappy.interfaces.snappyseries import ( | ||
417 | 35 | ISnappyDistroSeries, | ||
418 | 36 | ISnappyDistroSeriesSet, | ||
419 | 37 | ISnappySeries, | ||
420 | 38 | ISnappySeriesSet, | ||
421 | 39 | NoSuchSnappySeries, | ||
422 | 40 | ) | ||
423 | 41 | |||
424 | 42 | |||
425 | 43 | @implementer(ISnappySeries) | ||
426 | 44 | class SnappySeries(Storm): | ||
427 | 45 | """See `ISnappySeries`.""" | ||
428 | 46 | |||
429 | 47 | __storm_table__ = 'SnappySeries' | ||
430 | 48 | |||
431 | 49 | id = Int(primary=True) | ||
432 | 50 | |||
433 | 51 | date_created = DateTime( | ||
434 | 52 | name='date_created', tzinfo=pytz.UTC, allow_none=False) | ||
435 | 53 | |||
436 | 54 | registrant_id = Int(name='registrant', allow_none=False) | ||
437 | 55 | registrant = Reference(registrant_id, 'Person.id') | ||
438 | 56 | |||
439 | 57 | name = Unicode(name='name', allow_none=False) | ||
440 | 58 | |||
441 | 59 | display_name = Unicode(name='display_name', allow_none=False) | ||
442 | 60 | |||
443 | 61 | status = EnumCol(enum=SeriesStatus, notNull=True) | ||
444 | 62 | |||
445 | 63 | def __init__(self, registrant, name, display_name, status, | ||
446 | 64 | date_created=DEFAULT): | ||
447 | 65 | super(SnappySeries, self).__init__() | ||
448 | 66 | self.registrant = registrant | ||
449 | 67 | self.name = name | ||
450 | 68 | self.display_name = display_name | ||
451 | 69 | self.status = status | ||
452 | 70 | self.date_created = date_created | ||
453 | 71 | |||
454 | 72 | @property | ||
455 | 73 | def title(self): | ||
456 | 74 | return self.display_name | ||
457 | 75 | |||
458 | 76 | @property | ||
459 | 77 | def usable_distro_series(self): | ||
460 | 78 | rows = IStore(DistroSeries).find( | ||
461 | 79 | DistroSeries, | ||
462 | 80 | SnappyDistroSeries.snappy_series == self, | ||
463 | 81 | SnappyDistroSeries.distro_series_id == DistroSeries.id) | ||
464 | 82 | return rows.order_by(DistroSeries.id) | ||
465 | 83 | |||
466 | 84 | @usable_distro_series.setter | ||
467 | 85 | def usable_distro_series(self, value): | ||
468 | 86 | enablements = dict(Store.of(self).find( | ||
469 | 87 | (DistroSeries, SnappyDistroSeries), | ||
470 | 88 | SnappyDistroSeries.snappy_series == self, | ||
471 | 89 | SnappyDistroSeries.distro_series_id == DistroSeries.id)) | ||
472 | 90 | for distro_series in enablements: | ||
473 | 91 | if distro_series not in value: | ||
474 | 92 | Store.of(self).remove(enablements[distro_series]) | ||
475 | 93 | for distro_series in value: | ||
476 | 94 | if distro_series not in enablements: | ||
477 | 95 | link = SnappyDistroSeries(self, distro_series) | ||
478 | 96 | Store.of(self).add(link) | ||
479 | 97 | |||
480 | 98 | |||
481 | 99 | @implementer(ISnappyDistroSeries) | ||
482 | 100 | class SnappyDistroSeries(Storm): | ||
483 | 101 | """Link table between `SnappySeries` and `DistroSeries`.""" | ||
484 | 102 | |||
485 | 103 | __storm_table__ = 'SnappyDistroSeries' | ||
486 | 104 | __storm_primary__ = ('snappy_series_id', 'distro_series_id') | ||
487 | 105 | |||
488 | 106 | snappy_series_id = Int(name='snappy_series', allow_none=False) | ||
489 | 107 | snappy_series = Reference(snappy_series_id, 'SnappySeries.id') | ||
490 | 108 | |||
491 | 109 | distro_series_id = Int(name='distro_series', allow_none=False) | ||
492 | 110 | distro_series = Reference(distro_series_id, 'DistroSeries.id') | ||
493 | 111 | |||
494 | 112 | def __init__(self, snappy_series, distro_series): | ||
495 | 113 | super(SnappyDistroSeries, self).__init__() | ||
496 | 114 | self.snappy_series = snappy_series | ||
497 | 115 | self.distro_series = distro_series | ||
498 | 116 | |||
499 | 117 | @property | ||
500 | 118 | def title(self): | ||
501 | 119 | return "%s, for %s" % ( | ||
502 | 120 | self.distro_series.fullseriesname, self.snappy_series.title) | ||
503 | 121 | |||
504 | 122 | |||
505 | 123 | @implementer(ISnappySeriesSet) | ||
506 | 124 | class SnappySeriesSet: | ||
507 | 125 | """See `ISnappySeriesSet`.""" | ||
508 | 126 | |||
509 | 127 | def new(self, registrant, name, display_name, status, | ||
510 | 128 | date_created=DEFAULT): | ||
511 | 129 | """See `ISnappySeriesSet`.""" | ||
512 | 130 | store = IMasterStore(SnappySeries) | ||
513 | 131 | snappy_series = SnappySeries( | ||
514 | 132 | registrant, name, display_name, status, date_created=date_created) | ||
515 | 133 | store.add(snappy_series) | ||
516 | 134 | return snappy_series | ||
517 | 135 | |||
518 | 136 | def __iter__(self): | ||
519 | 137 | """See `ISnappySeriesSet`.""" | ||
520 | 138 | return iter(self.getAll()) | ||
521 | 139 | |||
522 | 140 | def __getitem__(self, name): | ||
523 | 141 | """See `ISnappySeriesSet`.""" | ||
524 | 142 | return self.getByName(name) | ||
525 | 143 | |||
526 | 144 | def getByName(self, name): | ||
527 | 145 | """See `ISnappySeriesSet`.""" | ||
528 | 146 | snappy_series = IStore(SnappySeries).find( | ||
529 | 147 | SnappySeries, SnappySeries.name == name).one() | ||
530 | 148 | if snappy_series is None: | ||
531 | 149 | raise NoSuchSnappySeries(name) | ||
532 | 150 | return snappy_series | ||
533 | 151 | |||
534 | 152 | def getByDistroSeries(self, distro_series): | ||
535 | 153 | """See `ISnappySeriesSet`.""" | ||
536 | 154 | rows = IStore(SnappySeries).find( | ||
537 | 155 | SnappySeries, | ||
538 | 156 | SnappyDistroSeries.snappy_series_id == SnappySeries.id, | ||
539 | 157 | SnappyDistroSeries.distro_series == distro_series) | ||
540 | 158 | return rows.order_by(Desc(SnappySeries.name)) | ||
541 | 159 | |||
542 | 160 | def getAll(self): | ||
543 | 161 | """See `ISnappySeriesSet`.""" | ||
544 | 162 | return IStore(SnappySeries).find(SnappySeries).order_by( | ||
545 | 163 | SnappySeries.name) | ||
546 | 164 | |||
547 | 165 | |||
548 | 166 | @implementer(ISnappyDistroSeriesSet) | ||
549 | 167 | class SnappyDistroSeriesSet: | ||
550 | 168 | """See `ISnappyDistroSeriesSet`.""" | ||
551 | 169 | |||
552 | 170 | def getByDistroSeries(self, distro_series): | ||
553 | 171 | """See `ISnappyDistroSeriesSet`.""" | ||
554 | 172 | store = IStore(SnappyDistroSeries) | ||
555 | 173 | rows = store.using(SnappyDistroSeries, SnappySeries).find( | ||
556 | 174 | SnappyDistroSeries, | ||
557 | 175 | SnappyDistroSeries.snappy_series_id == SnappySeries.id, | ||
558 | 176 | SnappyDistroSeries.distro_series == distro_series) | ||
559 | 177 | return rows.order_by(Desc(SnappySeries.name)) | ||
560 | 178 | |||
561 | 179 | def getByBothSeries(self, snappy_series, distro_series): | ||
562 | 180 | """See `ISnappyDistroSeriesSet`.""" | ||
563 | 181 | return IStore(SnappyDistroSeries).find( | ||
564 | 182 | SnappyDistroSeries, | ||
565 | 183 | SnappyDistroSeries.snappy_series == snappy_series, | ||
566 | 184 | SnappyDistroSeries.distro_series == distro_series).one() | ||
567 | 0 | 185 | ||
568 | === added file 'lib/lp/snappy/tests/test_snappyseries.py' | |||
569 | --- lib/lp/snappy/tests/test_snappyseries.py 1970-01-01 00:00:00 +0000 | |||
570 | +++ lib/lp/snappy/tests/test_snappyseries.py 2016-05-09 13:24:49 +0000 | |||
571 | @@ -0,0 +1,279 @@ | |||
572 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
573 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
574 | 3 | |||
575 | 4 | """Test snappy series.""" | ||
576 | 5 | |||
577 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
578 | 7 | |||
579 | 8 | __metaclass__ = type | ||
580 | 9 | |||
581 | 10 | from testtools.matchers import ( | ||
582 | 11 | MatchesSetwise, | ||
583 | 12 | MatchesStructure, | ||
584 | 13 | ) | ||
585 | 14 | from zope.component import getUtility | ||
586 | 15 | |||
587 | 16 | from lp.services.features.testing import FeatureFixture | ||
588 | 17 | from lp.services.webapp.interfaces import OAuthPermission | ||
589 | 18 | from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS | ||
590 | 19 | from lp.snappy.interfaces.snappyseries import ( | ||
591 | 20 | ISnappyDistroSeriesSet, | ||
592 | 21 | ISnappySeries, | ||
593 | 22 | ISnappySeriesSet, | ||
594 | 23 | NoSuchSnappySeries, | ||
595 | 24 | ) | ||
596 | 25 | from lp.testing import ( | ||
597 | 26 | admin_logged_in, | ||
598 | 27 | api_url, | ||
599 | 28 | logout, | ||
600 | 29 | person_logged_in, | ||
601 | 30 | TestCaseWithFactory, | ||
602 | 31 | ) | ||
603 | 32 | from lp.testing.layers import ( | ||
604 | 33 | DatabaseFunctionalLayer, | ||
605 | 34 | ZopelessDatabaseLayer, | ||
606 | 35 | ) | ||
607 | 36 | from lp.testing.pages import webservice_for_person | ||
608 | 37 | |||
609 | 38 | |||
610 | 39 | class TestSnappySeries(TestCaseWithFactory): | ||
611 | 40 | |||
612 | 41 | layer = ZopelessDatabaseLayer | ||
613 | 42 | |||
614 | 43 | def setUp(self): | ||
615 | 44 | super(TestSnappySeries, self).setUp() | ||
616 | 45 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
617 | 46 | |||
618 | 47 | def test_implements_interface(self): | ||
619 | 48 | # SnappySeries implements ISnappySeries. | ||
620 | 49 | snappy_series = self.factory.makeSnappySeries() | ||
621 | 50 | self.assertProvides(snappy_series, ISnappySeries) | ||
622 | 51 | |||
623 | 52 | def test_new_no_usable_distro_series(self): | ||
624 | 53 | snappy_series = self.factory.makeSnappySeries() | ||
625 | 54 | self.assertContentEqual([], snappy_series.usable_distro_series) | ||
626 | 55 | |||
627 | 56 | def test_set_usable_distro_series(self): | ||
628 | 57 | dses = [self.factory.makeDistroSeries() for _ in range(3)] | ||
629 | 58 | snappy_series = self.factory.makeSnappySeries( | ||
630 | 59 | usable_distro_series=[dses[0]]) | ||
631 | 60 | self.assertContentEqual([dses[0]], snappy_series.usable_distro_series) | ||
632 | 61 | snappy_series.usable_distro_series = dses | ||
633 | 62 | self.assertContentEqual(dses, snappy_series.usable_distro_series) | ||
634 | 63 | snappy_series.usable_distro_series = [] | ||
635 | 64 | self.assertContentEqual([], snappy_series.usable_distro_series) | ||
636 | 65 | |||
637 | 66 | |||
638 | 67 | class TestSnappySeriesSet(TestCaseWithFactory): | ||
639 | 68 | |||
640 | 69 | layer = ZopelessDatabaseLayer | ||
641 | 70 | |||
642 | 71 | def setUp(self): | ||
643 | 72 | super(TestSnappySeriesSet, self).setUp() | ||
644 | 73 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
645 | 74 | |||
646 | 75 | def test_getByName(self): | ||
647 | 76 | snappy_series = self.factory.makeSnappySeries(name="foo") | ||
648 | 77 | self.factory.makeSnappySeries() | ||
649 | 78 | snappy_series_set = getUtility(ISnappySeriesSet) | ||
650 | 79 | self.assertEqual(snappy_series, snappy_series_set.getByName("foo")) | ||
651 | 80 | self.assertRaises( | ||
652 | 81 | NoSuchSnappySeries, snappy_series_set.getByName, "bar") | ||
653 | 82 | |||
654 | 83 | def test_getByDistroSeries(self): | ||
655 | 84 | dses = [self.factory.makeDistroSeries() for _ in range(3)] | ||
656 | 85 | snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)] | ||
657 | 86 | snappy_serieses[0].usable_distro_series = dses | ||
658 | 87 | snappy_serieses[1].usable_distro_series = [dses[0], dses[1]] | ||
659 | 88 | snappy_serieses[2].usable_distro_series = [dses[1], dses[2]] | ||
660 | 89 | snappy_series_set = getUtility(ISnappySeriesSet) | ||
661 | 90 | self.assertContentEqual( | ||
662 | 91 | [snappy_serieses[0], snappy_serieses[1]], | ||
663 | 92 | snappy_series_set.getByDistroSeries(dses[0])) | ||
664 | 93 | self.assertContentEqual( | ||
665 | 94 | snappy_serieses, snappy_series_set.getByDistroSeries(dses[1])) | ||
666 | 95 | self.assertContentEqual( | ||
667 | 96 | [snappy_serieses[0], snappy_serieses[2]], | ||
668 | 97 | snappy_series_set.getByDistroSeries(dses[2])) | ||
669 | 98 | |||
670 | 99 | def test_getAll(self): | ||
671 | 100 | snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)] | ||
672 | 101 | self.assertContentEqual( | ||
673 | 102 | snappy_serieses, getUtility(ISnappySeriesSet).getAll()) | ||
674 | 103 | |||
675 | 104 | |||
676 | 105 | class TestSnappySeriesWebservice(TestCaseWithFactory): | ||
677 | 106 | |||
678 | 107 | layer = DatabaseFunctionalLayer | ||
679 | 108 | |||
680 | 109 | def setUp(self): | ||
681 | 110 | super(TestSnappySeriesWebservice, self).setUp() | ||
682 | 111 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
683 | 112 | |||
684 | 113 | def test_new_unpriv(self): | ||
685 | 114 | # An unprivileged user cannot create a SnappySeries. | ||
686 | 115 | person = self.factory.makePerson() | ||
687 | 116 | webservice = webservice_for_person( | ||
688 | 117 | person, permission=OAuthPermission.WRITE_PUBLIC) | ||
689 | 118 | webservice.default_api_version = "devel" | ||
690 | 119 | response = webservice.named_post( | ||
691 | 120 | "/+snappy-series", "new", | ||
692 | 121 | name="dummy", display_name="dummy", status="Experimental") | ||
693 | 122 | self.assertEqual(401, response.status) | ||
694 | 123 | |||
695 | 124 | def test_new(self): | ||
696 | 125 | # A registry expert can create a SnappySeries. | ||
697 | 126 | person = self.factory.makeRegistryExpert() | ||
698 | 127 | webservice = webservice_for_person( | ||
699 | 128 | person, permission=OAuthPermission.WRITE_PUBLIC) | ||
700 | 129 | webservice.default_api_version = "devel" | ||
701 | 130 | logout() | ||
702 | 131 | response = webservice.named_post( | ||
703 | 132 | "/+snappy-series", "new", | ||
704 | 133 | name="dummy", display_name="Dummy", status="Experimental") | ||
705 | 134 | self.assertEqual(201, response.status) | ||
706 | 135 | snappy_series = webservice.get( | ||
707 | 136 | response.getHeader("Location")).jsonBody() | ||
708 | 137 | with person_logged_in(person): | ||
709 | 138 | self.assertEqual( | ||
710 | 139 | webservice.getAbsoluteUrl(api_url(person)), | ||
711 | 140 | snappy_series["registrant_link"]) | ||
712 | 141 | self.assertEqual("dummy", snappy_series["name"]) | ||
713 | 142 | self.assertEqual("Dummy", snappy_series["display_name"]) | ||
714 | 143 | self.assertEqual("Experimental", snappy_series["status"]) | ||
715 | 144 | |||
716 | 145 | def test_new_duplicate_name(self): | ||
717 | 146 | # An attempt to create a SnappySeries with a duplicate name is | ||
718 | 147 | # rejected. | ||
719 | 148 | person = self.factory.makeRegistryExpert() | ||
720 | 149 | webservice = webservice_for_person( | ||
721 | 150 | person, permission=OAuthPermission.WRITE_PUBLIC) | ||
722 | 151 | webservice.default_api_version = "devel" | ||
723 | 152 | logout() | ||
724 | 153 | response = webservice.named_post( | ||
725 | 154 | "/+snappy-series", "new", | ||
726 | 155 | name="dummy", display_name="Dummy", status="Experimental") | ||
727 | 156 | self.assertEqual(201, response.status) | ||
728 | 157 | response = webservice.named_post( | ||
729 | 158 | "/+snappy-series", "new", | ||
730 | 159 | name="dummy", display_name="Dummy", status="Experimental") | ||
731 | 160 | self.assertEqual(400, response.status) | ||
732 | 161 | self.assertEqual( | ||
733 | 162 | "name: dummy is already in use by another series.", response.body) | ||
734 | 163 | |||
735 | 164 | def test_getByName(self): | ||
736 | 165 | # lp.snappy_serieses.getByName returns a matching SnappySeries. | ||
737 | 166 | person = self.factory.makePerson() | ||
738 | 167 | webservice = webservice_for_person( | ||
739 | 168 | person, permission=OAuthPermission.READ_PUBLIC) | ||
740 | 169 | webservice.default_api_version = "devel" | ||
741 | 170 | with admin_logged_in(): | ||
742 | 171 | self.factory.makeSnappySeries(name="dummy") | ||
743 | 172 | response = webservice.named_get( | ||
744 | 173 | "/+snappy-series", "getByName", name="dummy") | ||
745 | 174 | self.assertEqual(200, response.status) | ||
746 | 175 | self.assertEqual("dummy", response.jsonBody()["name"]) | ||
747 | 176 | |||
748 | 177 | def test_getByName_missing(self): | ||
749 | 178 | # lp.snappy_serieses.getByName returns 404 for a non-existent | ||
750 | 179 | # SnappySeries. | ||
751 | 180 | person = self.factory.makePerson() | ||
752 | 181 | webservice = webservice_for_person( | ||
753 | 182 | person, permission=OAuthPermission.READ_PUBLIC) | ||
754 | 183 | webservice.default_api_version = "devel" | ||
755 | 184 | logout() | ||
756 | 185 | response = webservice.named_get( | ||
757 | 186 | "/+snappy-series", "getByName", name="nonexistent") | ||
758 | 187 | self.assertEqual(404, response.status) | ||
759 | 188 | self.assertEqual( | ||
760 | 189 | "No such snappy series: 'nonexistent'.", response.body) | ||
761 | 190 | |||
762 | 191 | def test_getByDistroSeries(self): | ||
763 | 192 | # lp.snappy_serieses.getByDistroSeries returns a collection of | ||
764 | 193 | # matching SnappySeries. | ||
765 | 194 | person = self.factory.makePerson() | ||
766 | 195 | webservice = webservice_for_person( | ||
767 | 196 | person, permission=OAuthPermission.READ_PUBLIC) | ||
768 | 197 | webservice.default_api_version = "devel" | ||
769 | 198 | with admin_logged_in(): | ||
770 | 199 | dses = [self.factory.makeDistroSeries() for _ in range(3)] | ||
771 | 200 | ds_urls = [api_url(ds) for ds in dses] | ||
772 | 201 | snappy_serieses = [ | ||
773 | 202 | self.factory.makeSnappySeries(name="ss-%d" % i) | ||
774 | 203 | for i in range(3)] | ||
775 | 204 | snappy_serieses[0].usable_distro_series = dses | ||
776 | 205 | snappy_serieses[1].usable_distro_series = [dses[0], dses[1]] | ||
777 | 206 | snappy_serieses[2].usable_distro_series = [dses[1], dses[2]] | ||
778 | 207 | for ds_url, expected_snappy_series_names in ( | ||
779 | 208 | (ds_urls[0], ["ss-0", "ss-1"]), | ||
780 | 209 | (ds_urls[1], ["ss-0", "ss-1", "ss-2"]), | ||
781 | 210 | (ds_urls[2], ["ss-0", "ss-2"])): | ||
782 | 211 | response = webservice.named_get( | ||
783 | 212 | "/+snappy-series", "getByDistroSeries", distro_series=ds_url) | ||
784 | 213 | self.assertEqual(200, response.status) | ||
785 | 214 | self.assertContentEqual( | ||
786 | 215 | expected_snappy_series_names, | ||
787 | 216 | [entry["name"] for entry in response.jsonBody()["entries"]]) | ||
788 | 217 | |||
789 | 218 | def test_collection(self): | ||
790 | 219 | # lp.snappy_serieses is a collection of all SnappySeries. | ||
791 | 220 | person = self.factory.makePerson() | ||
792 | 221 | webservice = webservice_for_person( | ||
793 | 222 | person, permission=OAuthPermission.READ_PUBLIC) | ||
794 | 223 | webservice.default_api_version = "devel" | ||
795 | 224 | with admin_logged_in(): | ||
796 | 225 | for i in range(3): | ||
797 | 226 | self.factory.makeSnappySeries(name="ss-%d" % i) | ||
798 | 227 | response = webservice.get("/+snappy-series") | ||
799 | 228 | self.assertEqual(200, response.status) | ||
800 | 229 | self.assertContentEqual( | ||
801 | 230 | ["ss-0", "ss-1", "ss-2"], | ||
802 | 231 | [entry["name"] for entry in response.jsonBody()["entries"]]) | ||
803 | 232 | |||
804 | 233 | |||
805 | 234 | class TestSnappyDistroSeriesSet(TestCaseWithFactory): | ||
806 | 235 | |||
807 | 236 | layer = ZopelessDatabaseLayer | ||
808 | 237 | |||
809 | 238 | def setUp(self): | ||
810 | 239 | super(TestSnappyDistroSeriesSet, self).setUp() | ||
811 | 240 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
812 | 241 | |||
813 | 242 | def test_getByDistroSeries(self): | ||
814 | 243 | dses = [self.factory.makeDistroSeries() for _ in range(3)] | ||
815 | 244 | snappy_serieses = [self.factory.makeSnappySeries() for _ in range(3)] | ||
816 | 245 | snappy_serieses[0].usable_distro_series = dses | ||
817 | 246 | snappy_serieses[1].usable_distro_series = [dses[0], dses[1]] | ||
818 | 247 | snappy_serieses[2].usable_distro_series = [dses[1], dses[2]] | ||
819 | 248 | sds_set = getUtility(ISnappyDistroSeriesSet) | ||
820 | 249 | self.assertThat( | ||
821 | 250 | sds_set.getByDistroSeries(dses[0]), | ||
822 | 251 | MatchesSetwise(*( | ||
823 | 252 | MatchesStructure.byEquality( | ||
824 | 253 | snappy_series=ss, distro_series=dses[0]) | ||
825 | 254 | for ss in (snappy_serieses[0], snappy_serieses[1])))) | ||
826 | 255 | self.assertThat( | ||
827 | 256 | sds_set.getByDistroSeries(dses[1]), | ||
828 | 257 | MatchesSetwise(*( | ||
829 | 258 | MatchesStructure.byEquality( | ||
830 | 259 | snappy_series=ss, distro_series=dses[1]) | ||
831 | 260 | for ss in snappy_serieses))) | ||
832 | 261 | self.assertThat( | ||
833 | 262 | sds_set.getByDistroSeries(dses[2]), | ||
834 | 263 | MatchesSetwise(*( | ||
835 | 264 | MatchesStructure.byEquality( | ||
836 | 265 | snappy_series=ss, distro_series=dses[2]) | ||
837 | 266 | for ss in (snappy_serieses[0], snappy_serieses[2])))) | ||
838 | 267 | |||
839 | 268 | def test_getByBothSeries(self): | ||
840 | 269 | dses = [self.factory.makeDistroSeries() for _ in range(2)] | ||
841 | 270 | snappy_serieses = [self.factory.makeSnappySeries() for _ in range(2)] | ||
842 | 271 | snappy_serieses[0].usable_distro_series = [dses[0]] | ||
843 | 272 | sds_set = getUtility(ISnappyDistroSeriesSet) | ||
844 | 273 | self.assertThat( | ||
845 | 274 | sds_set.getByBothSeries(snappy_serieses[0], dses[0]), | ||
846 | 275 | MatchesStructure.byEquality( | ||
847 | 276 | snappy_series=snappy_serieses[0], distro_series=dses[0])) | ||
848 | 277 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1])) | ||
849 | 278 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0])) | ||
850 | 279 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1])) | ||
851 | 0 | 280 | ||
852 | === modified file 'lib/lp/snappy/vocabularies.py' | |||
853 | --- lib/lp/snappy/vocabularies.py 2015-09-18 14:14:34 +0000 | |||
854 | +++ lib/lp/snappy/vocabularies.py 2016-05-09 13:24:49 +0000 | |||
855 | @@ -1,4 +1,4 @@ | |||
857 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the GNU | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the GNU |
858 | 2 | # Affero General Public License version 3 (see the file LICENSE). | 2 | # Affero General Public License version 3 (see the file LICENSE). |
859 | 3 | 3 | ||
860 | 4 | """Snappy vocabularies.""" | 4 | """Snappy vocabularies.""" |
861 | @@ -7,11 +7,21 @@ | |||
862 | 7 | 7 | ||
863 | 8 | __all__ = [ | 8 | __all__ = [ |
864 | 9 | 'SnapDistroArchSeriesVocabulary', | 9 | 'SnapDistroArchSeriesVocabulary', |
865 | 10 | 'SnappySeriesVocabulary', | ||
866 | 10 | ] | 11 | ] |
867 | 11 | 12 | ||
868 | 13 | from storm.locals import Desc | ||
869 | 12 | from zope.schema.vocabulary import SimpleTerm | 14 | from zope.schema.vocabulary import SimpleTerm |
870 | 13 | 15 | ||
871 | 16 | from lp.registry.model.distribution import Distribution | ||
872 | 17 | from lp.registry.model.distroseries import DistroSeries | ||
873 | 18 | from lp.registry.model.series import ACTIVE_STATUSES | ||
874 | 19 | from lp.services.database.interfaces import IStore | ||
875 | 14 | from lp.services.webapp.vocabulary import StormVocabularyBase | 20 | from lp.services.webapp.vocabulary import StormVocabularyBase |
876 | 21 | from lp.snappy.model.snappyseries import ( | ||
877 | 22 | SnappyDistroSeries, | ||
878 | 23 | SnappySeries, | ||
879 | 24 | ) | ||
880 | 15 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 25 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
881 | 16 | 26 | ||
882 | 17 | 27 | ||
883 | @@ -29,3 +39,73 @@ | |||
884 | 29 | 39 | ||
885 | 30 | def __len__(self): | 40 | def __len__(self): |
886 | 31 | return len(self.context.getAllowedArchitectures()) | 41 | return len(self.context.getAllowedArchitectures()) |
887 | 42 | |||
888 | 43 | |||
889 | 44 | class SnappySeriesVocabulary(StormVocabularyBase): | ||
890 | 45 | """A vocabulary for searching snappy series.""" | ||
891 | 46 | |||
892 | 47 | _table = SnappySeries | ||
893 | 48 | _clauses = [SnappySeries.status.is_in(ACTIVE_STATUSES)] | ||
894 | 49 | _order_by = Desc(SnappySeries.date_created) | ||
895 | 50 | |||
896 | 51 | |||
897 | 52 | class SnappyDistroSeriesVocabulary(StormVocabularyBase): | ||
898 | 53 | """A vocabulary for searching snappy/distro series combinations.""" | ||
899 | 54 | |||
900 | 55 | _table = SnappyDistroSeries | ||
901 | 56 | _clauses = [ | ||
902 | 57 | SnappyDistroSeries.snappy_series_id == SnappySeries.id, | ||
903 | 58 | SnappyDistroSeries.distro_series_id == DistroSeries.id, | ||
904 | 59 | DistroSeries.distributionID == Distribution.id, | ||
905 | 60 | ] | ||
906 | 61 | |||
907 | 62 | @property | ||
908 | 63 | def _entries(self): | ||
909 | 64 | tables = [SnappyDistroSeries, SnappySeries, DistroSeries, Distribution] | ||
910 | 65 | entries = IStore(self._table).using(*tables).find( | ||
911 | 66 | self._table, *self._clauses) | ||
912 | 67 | return entries.order_by( | ||
913 | 68 | Distribution.display_name, Desc(DistroSeries.date_created), | ||
914 | 69 | Desc(SnappySeries.date_created)) | ||
915 | 70 | |||
916 | 71 | def toTerm(self, obj): | ||
917 | 72 | """See `IVocabulary`.""" | ||
918 | 73 | token = "%s/%s/%s" % ( | ||
919 | 74 | obj.distro_series.distribution.name, obj.distro_series.name, | ||
920 | 75 | obj.snappy_series.name) | ||
921 | 76 | return SimpleTerm(obj, token, obj.title) | ||
922 | 77 | |||
923 | 78 | def __contains__(self, value): | ||
924 | 79 | """See `IVocabulary`.""" | ||
925 | 80 | return value in self._entries | ||
926 | 81 | |||
927 | 82 | def getTerm(self, value): | ||
928 | 83 | """See `IVocabulary`.""" | ||
929 | 84 | if value not in self: | ||
930 | 85 | raise LookupError(value) | ||
931 | 86 | return self.toTerm(value) | ||
932 | 87 | |||
933 | 88 | def getTermByToken(self, token): | ||
934 | 89 | """See `IVocabularyTokenized`.""" | ||
935 | 90 | try: | ||
936 | 91 | distribution_name, distro_series_name, snappy_series_name = ( | ||
937 | 92 | token.split("/", 2)) | ||
938 | 93 | except ValueError: | ||
939 | 94 | raise LookupError(token) | ||
940 | 95 | entry = IStore(self._table).find( | ||
941 | 96 | self._table, | ||
942 | 97 | Distribution.name == distribution_name, | ||
943 | 98 | DistroSeries.name == distro_series_name, | ||
944 | 99 | SnappySeries.name == snappy_series_name, | ||
945 | 100 | *self._clauses).one() | ||
946 | 101 | if entry is None: | ||
947 | 102 | raise LookupError(token) | ||
948 | 103 | return self.toTerm(entry) | ||
949 | 104 | |||
950 | 105 | |||
951 | 106 | class BuildableSnappyDistroSeriesVocabulary(SnappyDistroSeriesVocabulary): | ||
952 | 107 | """A vocabulary for searching active snappy/distro series combinations.""" | ||
953 | 108 | |||
954 | 109 | _clauses = SnappyDistroSeriesVocabulary._clauses + [ | ||
955 | 110 | SnappySeries.status.is_in(ACTIVE_STATUSES), | ||
956 | 111 | ] | ||
957 | 32 | 112 | ||
958 | === modified file 'lib/lp/snappy/vocabularies.zcml' | |||
959 | --- lib/lp/snappy/vocabularies.zcml 2015-09-18 13:32:09 +0000 | |||
960 | +++ lib/lp/snappy/vocabularies.zcml 2016-05-09 13:24:49 +0000 | |||
961 | @@ -1,4 +1,4 @@ | |||
963 | 1 | <!-- Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
964 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
965 | 3 | --> | 3 | --> |
966 | 4 | 4 | ||
967 | @@ -15,4 +15,37 @@ | |||
968 | 15 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> | 15 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> |
969 | 16 | </class> | 16 | </class> |
970 | 17 | 17 | ||
971 | 18 | <securedutility | ||
972 | 19 | name="SnappySeries" | ||
973 | 20 | component="lp.snappy.vocabularies.SnappySeriesVocabulary" | ||
974 | 21 | provides="zope.schema.interfaces.IVocabularyFactory"> | ||
975 | 22 | <allow interface="zope.schema.interfaces.IVocabularyFactory" /> | ||
976 | 23 | </securedutility> | ||
977 | 24 | |||
978 | 25 | <class class="lp.snappy.vocabularies.SnappySeriesVocabulary"> | ||
979 | 26 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> | ||
980 | 27 | </class> | ||
981 | 28 | |||
982 | 29 | <securedutility | ||
983 | 30 | name="SnappyDistroSeries" | ||
984 | 31 | component="lp.snappy.vocabularies.SnappyDistroSeriesVocabulary" | ||
985 | 32 | provides="zope.schema.interfaces.IVocabularyFactory"> | ||
986 | 33 | <allow interface="zope.schema.interfaces.IVocabularyFactory" /> | ||
987 | 34 | </securedutility> | ||
988 | 35 | |||
989 | 36 | <class class="lp.snappy.vocabularies.SnappyDistroSeriesVocabulary"> | ||
990 | 37 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> | ||
991 | 38 | </class> | ||
992 | 39 | |||
993 | 40 | <securedutility | ||
994 | 41 | name="BuildableSnappyDistroSeries" | ||
995 | 42 | component="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary" | ||
996 | 43 | provides="zope.schema.interfaces.IVocabularyFactory"> | ||
997 | 44 | <allow interface="zope.schema.interfaces.IVocabularyFactory" /> | ||
998 | 45 | </securedutility> | ||
999 | 46 | |||
1000 | 47 | <class class="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary"> | ||
1001 | 48 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> | ||
1002 | 49 | </class> | ||
1003 | 50 | |||
1004 | 18 | </configure> | 51 | </configure> |
1005 | 19 | 52 | ||
1006 | === modified file 'lib/lp/testing/factory.py' | |||
1007 | --- lib/lp/testing/factory.py 2016-04-28 02:25:46 +0000 | |||
1008 | +++ lib/lp/testing/factory.py 2016-05-09 13:24:49 +0000 | |||
1009 | @@ -275,6 +275,7 @@ | |||
1010 | 275 | from lp.services.worlddata.interfaces.language import ILanguageSet | 275 | from lp.services.worlddata.interfaces.language import ILanguageSet |
1011 | 276 | from lp.snappy.interfaces.snap import ISnapSet | 276 | from lp.snappy.interfaces.snap import ISnapSet |
1012 | 277 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet | 277 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
1013 | 278 | from lp.snappy.interfaces.snappyseries import ISnappySeriesSet | ||
1014 | 278 | from lp.snappy.model.snapbuild import SnapFile | 279 | from lp.snappy.model.snapbuild import SnapFile |
1015 | 279 | from lp.soyuz.adapters.overrides import SourceOverride | 280 | from lp.soyuz.adapters.overrides import SourceOverride |
1016 | 280 | from lp.soyuz.adapters.packagelocation import PackageLocation | 281 | from lp.soyuz.adapters.packagelocation import PackageLocation |
1017 | @@ -4679,6 +4680,24 @@ | |||
1018 | 4679 | return ProxyFactory( | 4680 | return ProxyFactory( |
1019 | 4680 | SnapFile(snapbuild=snapbuild, libraryfile=libraryfile)) | 4681 | SnapFile(snapbuild=snapbuild, libraryfile=libraryfile)) |
1020 | 4681 | 4682 | ||
1021 | 4683 | def makeSnappySeries(self, registrant=None, name=None, display_name=None, | ||
1022 | 4684 | status=SeriesStatus.DEVELOPMENT, | ||
1023 | 4685 | date_created=DEFAULT, usable_distro_series=None): | ||
1024 | 4686 | """Make a new SnappySeries.""" | ||
1025 | 4687 | if registrant is None: | ||
1026 | 4688 | registrant = self.makePerson() | ||
1027 | 4689 | if name is None: | ||
1028 | 4690 | name = self.getUniqueString(u"snappy-series-name") | ||
1029 | 4691 | if display_name is None: | ||
1030 | 4692 | display_name = SPACE.join( | ||
1031 | 4693 | word.capitalize() for word in name.split('-')) | ||
1032 | 4694 | snappy_series = getUtility(ISnappySeriesSet).new( | ||
1033 | 4695 | registrant, name, display_name, status, date_created=date_created) | ||
1034 | 4696 | if usable_distro_series is not None: | ||
1035 | 4697 | snappy_series.usable_distro_series = usable_distro_series | ||
1036 | 4698 | IStore(snappy_series).flush() | ||
1037 | 4699 | return snappy_series | ||
1038 | 4700 | |||
1039 | 4682 | 4701 | ||
1040 | 4683 | # Some factory methods return simple Python types. We don't add | 4702 | # Some factory methods return simple Python types. We don't add |
1041 | 4684 | # security wrappers for them, as well as for objects created by | 4703 | # security wrappers for them, as well as for objects created by |