Merge lp:~cjwatson/launchpad/snappy-series-can-guess-distro-series into lp:launchpad
- snappy-series-can-guess-distro-series
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18885 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snappy-series-can-guess-distro-series | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-without-distro-series | ||||
Diff against target: |
414 lines (+185/-32) 7 files modified
lib/lp/snappy/browser/snap.py (+3/-1) lib/lp/snappy/browser/tests/test_snap.py (+64/-0) lib/lp/snappy/interfaces/snappyseries.py (+9/-3) lib/lp/snappy/model/snappyseries.py (+34/-5) lib/lp/snappy/tests/test_snappyseries.py (+27/-2) lib/lp/snappy/vocabularies.py (+44/-20) lib/lp/testing/factory.py (+4/-1) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snappy-series-can-guess-distro-series | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+362731@code.launchpad.net |
Commit message
Support SnappySeries that can guess the distro series from snapcraft.yaml.
Description of the change
This lets us offer better defaults when creating or editing a Snap.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
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/snappy/browser/snap.py' |
2 | --- lib/lp/snappy/browser/snap.py 2019-02-07 16:35:34 +0000 |
3 | +++ lib/lp/snappy/browser/snap.py 2019-02-07 16:35:34 +0000 |
4 | @@ -490,7 +490,9 @@ |
5 | store_name = snapcraft_data.get('name') |
6 | |
7 | store_series = getUtility(ISnappySeriesSet).getAll().first() |
8 | - if store_series.preferred_distro_series is not None: |
9 | + if store_series.can_infer_distro_series: |
10 | + distro_series = None |
11 | + elif store_series.preferred_distro_series is not None: |
12 | distro_series = store_series.preferred_distro_series |
13 | else: |
14 | distro_series = store_series.usable_distro_series.first() |
15 | |
16 | === modified file 'lib/lp/snappy/browser/tests/test_snap.py' |
17 | --- lib/lp/snappy/browser/tests/test_snap.py 2019-02-07 16:35:34 +0000 |
18 | +++ lib/lp/snappy/browser/tests/test_snap.py 2019-02-07 16:35:34 +0000 |
19 | @@ -32,6 +32,7 @@ |
20 | MatchesListwise, |
21 | MatchesSetwise, |
22 | MatchesStructure, |
23 | + Not, |
24 | ) |
25 | import transaction |
26 | from zope.component import getUtility |
27 | @@ -226,6 +227,24 @@ |
28 | MatchesStructure.byEquality( |
29 | snappy_series=newest, distro_series=lts)) |
30 | |
31 | + def test_initial_store_distro_series_can_infer_distro_series(self): |
32 | + # If the latest snappy series supports inferring the distro series |
33 | + # from snapcraft.yaml, then we default to that. |
34 | + self.useFixture(BranchHostingFixture(blob=b"")) |
35 | + lts = self.factory.makeUbuntuDistroSeries( |
36 | + version="16.04", status=SeriesStatus.CURRENT) |
37 | + with admin_logged_in(): |
38 | + self.factory.makeSnappySeries(usable_distro_series=[lts]) |
39 | + newest = self.factory.makeSnappySeries( |
40 | + preferred_distro_series=lts, can_infer_distro_series=True) |
41 | + branch = self.factory.makeAnyBranch() |
42 | + with person_logged_in(self.person): |
43 | + view = create_initialized_view(branch, "+new-snap") |
44 | + self.assertThat( |
45 | + view.initial_values["store_distro_series"], |
46 | + MatchesStructure( |
47 | + snappy_series=Equals(newest), distro_series=Is(None))) |
48 | + |
49 | def test_create_new_snap_not_logged_in(self): |
50 | branch = self.factory.makeAnyBranch() |
51 | self.assertRaises( |
52 | @@ -532,6 +551,26 @@ |
53 | self.assertContentEqual( |
54 | ["386", "amd64"], [proc.name for proc in snap.processors]) |
55 | |
56 | + def test_create_new_snap_infer_distro_series(self): |
57 | + self.useFixture(BranchHostingFixture(blob=b"")) |
58 | + with admin_logged_in(): |
59 | + self.snappyseries.can_infer_distro_series = True |
60 | + branch = self.factory.makeAnyBranch() |
61 | + browser = self.getViewBrowser( |
62 | + branch, view_name="+new-snap", user=self.person) |
63 | + browser.getControl(name="field.name").value = "snap-name" |
64 | + self.assertEqual( |
65 | + [self.snappyseries.name], |
66 | + browser.getControl(name="field.store_distro_series").value) |
67 | + self.assertEqual( |
68 | + self.snappyseries.name, |
69 | + browser.getControl(name="field.store_distro_series").options[0]) |
70 | + browser.getControl("Create snap package").click() |
71 | + |
72 | + content = find_main_content(browser.contents) |
73 | + self.assertEqual("snap-name", extract_text(content.h1)) |
74 | + self.assertIsNone(find_tag_by_id(content, "distro_series")) |
75 | + |
76 | def test_initial_name_extraction_bzr_success(self): |
77 | self.useFixture(BranchHostingFixture( |
78 | file_list={"snapcraft.yaml": "file-id"}, blob=b"name: test-snap")) |
79 | @@ -1404,6 +1443,31 @@ |
80 | store_upload_tag, |
81 | soupmatchers.Tag("store name", "span", text=snap.store_name)))) |
82 | |
83 | + def test_index_store_upload_no_distro_series(self): |
84 | + # If the snap package is to be automatically uploaded to the store |
85 | + # and is configured to infer an appropriate distro series from |
86 | + # snapcraft.yaml, the index page shows details of this. |
87 | + with admin_logged_in(): |
88 | + snappyseries = self.factory.makeSnappySeries( |
89 | + usable_distro_series=[self.distroseries], |
90 | + can_infer_distro_series=True) |
91 | + snap = self.makeSnap( |
92 | + distroseries=None, store_upload=True, store_series=snappyseries, |
93 | + store_name=self.getUniqueString("store-name")) |
94 | + view = create_initialized_view(snap, "+index") |
95 | + store_upload_tag = soupmatchers.Tag( |
96 | + "store upload", "div", attrs={"id": "store_upload"}) |
97 | + self.assertThat(view(), soupmatchers.HTMLContains( |
98 | + Not(soupmatchers.Tag( |
99 | + "distribution series", "dl", attrs={"id": "distro_series"})), |
100 | + soupmatchers.Within( |
101 | + store_upload_tag, |
102 | + soupmatchers.Tag( |
103 | + "store series name", "span", text=snappyseries.title)), |
104 | + soupmatchers.Within( |
105 | + store_upload_tag, |
106 | + soupmatchers.Tag("store name", "span", text=snap.store_name)))) |
107 | + |
108 | def setStatus(self, build, status): |
109 | build.updateStatus( |
110 | BuildStatus.BUILDING, date_started=build.date_created) |
111 | |
112 | === modified file 'lib/lp/snappy/interfaces/snappyseries.py' |
113 | --- lib/lp/snappy/interfaces/snappyseries.py 2019-01-30 18:00:09 +0000 |
114 | +++ lib/lp/snappy/interfaces/snappyseries.py 2019-02-07 16:35:34 +0000 |
115 | @@ -1,4 +1,4 @@ |
116 | -# Copyright 2016 Canonical Ltd. This software is licensed under the |
117 | +# Copyright 2016-2019 Canonical Ltd. This software is licensed under the |
118 | # GNU Affero General Public License version 3 (see the file LICENSE). |
119 | |
120 | """Snappy series interfaces.""" |
121 | @@ -86,7 +86,7 @@ |
122 | registrant = exported(PublicPersonChoice( |
123 | title=_("Registrant"), required=True, readonly=True, |
124 | vocabulary="ValidPersonOrTeam", |
125 | - description=_("The person who registered this snap package."))) |
126 | + description=_("The person who registered this snappy series."))) |
127 | |
128 | |
129 | class ISnappySeriesEditableAttributes(Interface): |
130 | @@ -118,6 +118,12 @@ |
131 | value_type=Reference(schema=IDistroSeries), |
132 | required=True, readonly=False)) |
133 | |
134 | + can_infer_distro_series = exported(Bool( |
135 | + title=_("Can infer distro series?"), required=True, readonly=False, |
136 | + description=_( |
137 | + "True if inferring a distro series from snapcraft.yaml is " |
138 | + "supported for this snappy series."))) |
139 | + |
140 | |
141 | class ISnappySeries(ISnappySeriesView, ISnappySeriesEditableAttributes): |
142 | """A series for snap packages in the store.""" |
143 | @@ -134,7 +140,7 @@ |
144 | snappy_series = Reference( |
145 | ISnappySeries, title=_("Snappy series"), readonly=True) |
146 | distro_series = Reference( |
147 | - IDistroSeries, title=_("Distro series"), readonly=True) |
148 | + IDistroSeries, title=_("Distro series"), required=False, readonly=True) |
149 | preferred = Bool( |
150 | title=_("Preferred"), |
151 | required=True, readonly=False, |
152 | |
153 | === modified file 'lib/lp/snappy/model/snappyseries.py' |
154 | --- lib/lp/snappy/model/snappyseries.py 2019-01-30 13:57:06 +0000 |
155 | +++ lib/lp/snappy/model/snappyseries.py 2019-02-07 16:35:34 +0000 |
156 | @@ -1,4 +1,4 @@ |
157 | -# Copyright 2016 Canonical Ltd. This software is licensed under the |
158 | +# Copyright 2016-2019 Canonical Ltd. This software is licensed under the |
159 | # GNU Affero General Public License version 3 (see the file LICENSE). |
160 | |
161 | """Snappy series.""" |
162 | @@ -138,18 +138,44 @@ |
163 | link = SnappyDistroSeries(self, distro_series) |
164 | Store.of(self).add(link) |
165 | |
166 | + @cachedproperty |
167 | + def _can_infer_distro_series(self): |
168 | + return not Store.of(self).find( |
169 | + SnappyDistroSeries, |
170 | + SnappyDistroSeries.snappy_series == self, |
171 | + SnappyDistroSeries.distro_series == None).is_empty() |
172 | + |
173 | + @property |
174 | + def can_infer_distro_series(self): |
175 | + return self._can_infer_distro_series |
176 | + |
177 | + @can_infer_distro_series.setter |
178 | + def can_infer_distro_series(self, value): |
179 | + store = Store.of(self) |
180 | + current = store.find( |
181 | + SnappyDistroSeries, |
182 | + SnappyDistroSeries.snappy_series == self, |
183 | + SnappyDistroSeries.distro_series == None).one() |
184 | + if current is None and value is True: |
185 | + store.add(SnappyDistroSeries(self, None)) |
186 | + get_property_cache(self)._can_infer_distro_series = True |
187 | + elif current is not None and value is False: |
188 | + store.remove(current) |
189 | + get_property_cache(self)._can_infer_distro_series = False |
190 | + |
191 | |
192 | @implementer(ISnappyDistroSeries) |
193 | class SnappyDistroSeries(Storm): |
194 | """Link table between `SnappySeries` and `DistroSeries`.""" |
195 | |
196 | __storm_table__ = 'SnappyDistroSeries' |
197 | - __storm_primary__ = ('snappy_series_id', 'distro_series_id') |
198 | + |
199 | + id = Int(primary=True) |
200 | |
201 | snappy_series_id = Int(name='snappy_series', allow_none=False) |
202 | snappy_series = Reference(snappy_series_id, 'SnappySeries.id') |
203 | |
204 | - distro_series_id = Int(name='distro_series', allow_none=False) |
205 | + distro_series_id = Int(name='distro_series', allow_none=True) |
206 | distro_series = Reference(distro_series_id, 'DistroSeries.id') |
207 | |
208 | preferred = Bool(name='preferred', allow_none=False) |
209 | @@ -162,8 +188,11 @@ |
210 | |
211 | @property |
212 | def title(self): |
213 | - return "%s, for %s" % ( |
214 | - self.distro_series.fullseriesname, self.snappy_series.title) |
215 | + if self.distro_series is not None: |
216 | + return "%s, for %s" % ( |
217 | + self.distro_series.fullseriesname, self.snappy_series.title) |
218 | + else: |
219 | + return self.snappy_series.title |
220 | |
221 | |
222 | @implementer(ISnappySeriesSet) |
223 | |
224 | === modified file 'lib/lp/snappy/tests/test_snappyseries.py' |
225 | --- lib/lp/snappy/tests/test_snappyseries.py 2019-01-30 18:00:09 +0000 |
226 | +++ lib/lp/snappy/tests/test_snappyseries.py 2019-02-07 16:35:34 +0000 |
227 | @@ -1,4 +1,4 @@ |
228 | -# Copyright 2016 Canonical Ltd. This software is licensed under the |
229 | +# Copyright 2016-2019 Canonical Ltd. This software is licensed under the |
230 | # GNU Affero General Public License version 3 (see the file LICENSE). |
231 | |
232 | """Test snappy series.""" |
233 | @@ -86,6 +86,20 @@ |
234 | self.assertContentEqual([], snappy_series.usable_distro_series) |
235 | self.assertIsNone(snappy_series.preferred_distro_series) |
236 | |
237 | + def test_can_infer_distro_series(self): |
238 | + # Setting can_infer_distro_series indicates that this snappy series |
239 | + # supports inferring the distro series from snapcraft.yaml. |
240 | + snappy_series = self.factory.makeSnappySeries() |
241 | + sds_set = getUtility(ISnappyDistroSeriesSet) |
242 | + self.assertFalse(snappy_series.can_infer_distro_series) |
243 | + self.assertIsNone(sds_set.getByBothSeries(snappy_series, None)) |
244 | + snappy_series.can_infer_distro_series = True |
245 | + self.assertTrue(snappy_series.can_infer_distro_series) |
246 | + self.assertIsNotNone(sds_set.getByBothSeries(snappy_series, None)) |
247 | + snappy_series.can_infer_distro_series = False |
248 | + self.assertFalse(snappy_series.can_infer_distro_series) |
249 | + self.assertIsNone(sds_set.getByBothSeries(snappy_series, None)) |
250 | + |
251 | def test_anonymous(self): |
252 | # Anyone can view an `ISnappySeries`. |
253 | series = self.factory.makeSnappySeries(name="dummy") |
254 | @@ -235,10 +249,21 @@ |
255 | self.assertThat( |
256 | sds_set.getByBothSeries(snappy_serieses[0], dses[0]), |
257 | MatchesStructure.byEquality( |
258 | - snappy_series=snappy_serieses[0], distro_series=dses[0])) |
259 | + snappy_series=snappy_serieses[0], distro_series=dses[0], |
260 | + title="%s, for %s" % ( |
261 | + dses[0].fullseriesname, snappy_serieses[0].title))) |
262 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1])) |
263 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0])) |
264 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1])) |
265 | + self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], None)) |
266 | + self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], None)) |
267 | + snappy_serieses[0].can_infer_distro_series = True |
268 | + self.assertThat( |
269 | + sds_set.getByBothSeries(snappy_serieses[0], None), |
270 | + MatchesStructure.byEquality( |
271 | + snappy_series=snappy_serieses[0], distro_series=None, |
272 | + title=snappy_serieses[0].title)) |
273 | + self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], None)) |
274 | |
275 | def test_getAll(self): |
276 | sdses = list(IStore(SnappyDistroSeries).find(SnappyDistroSeries)) |
277 | |
278 | === modified file 'lib/lp/snappy/vocabularies.py' |
279 | --- lib/lp/snappy/vocabularies.py 2017-01-04 20:52:12 +0000 |
280 | +++ lib/lp/snappy/vocabularies.py 2019-02-07 16:35:34 +0000 |
281 | @@ -1,4 +1,4 @@ |
282 | -# Copyright 2015-2017 Canonical Ltd. This software is licensed under the GNU |
283 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the GNU |
284 | # Affero General Public License version 3 (see the file LICENSE). |
285 | |
286 | """Snappy vocabularies.""" |
287 | @@ -6,12 +6,20 @@ |
288 | __metaclass__ = type |
289 | |
290 | __all__ = [ |
291 | + 'BuildableSnappyDistroSeriesVocabulary', |
292 | 'SnapDistroArchSeriesVocabulary', |
293 | + 'SnappyDistroSeriesVocabulary', |
294 | 'SnappySeriesVocabulary', |
295 | + 'SnapStoreChannel', |
296 | + 'SnapStoreChannelVocabulary', |
297 | ] |
298 | |
299 | from lazr.restful.interfaces import IJSONPublishable |
300 | -from storm.locals import Desc |
301 | +from storm.expr import LeftJoin |
302 | +from storm.locals import ( |
303 | + Desc, |
304 | + Not, |
305 | + ) |
306 | from zope.component import getUtility |
307 | from zope.interface import implementer |
308 | from zope.schema.vocabulary import ( |
309 | @@ -24,6 +32,10 @@ |
310 | from lp.registry.model.distroseries import DistroSeries |
311 | from lp.registry.model.series import ACTIVE_STATUSES |
312 | from lp.services.database.interfaces import IStore |
313 | +from lp.services.database.stormexpr import ( |
314 | + IsDistinctFrom, |
315 | + NullsFirst, |
316 | + ) |
317 | from lp.services.webapp.vocabulary import StormVocabularyBase |
318 | from lp.snappy.interfaces.snap import ISnap |
319 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient |
320 | @@ -62,26 +74,33 @@ |
321 | """A vocabulary for searching snappy/distro series combinations.""" |
322 | |
323 | _table = SnappyDistroSeries |
324 | - _clauses = [ |
325 | - SnappyDistroSeries.snappy_series_id == SnappySeries.id, |
326 | - SnappyDistroSeries.distro_series_id == DistroSeries.id, |
327 | - DistroSeries.distributionID == Distribution.id, |
328 | + _origin = [ |
329 | + SnappyDistroSeries, |
330 | + LeftJoin( |
331 | + DistroSeries, |
332 | + SnappyDistroSeries.distro_series_id == DistroSeries.id), |
333 | + LeftJoin(Distribution, DistroSeries.distributionID == Distribution.id), |
334 | + SnappySeries, |
335 | ] |
336 | + _clauses = [SnappyDistroSeries.snappy_series_id == SnappySeries.id] |
337 | |
338 | @property |
339 | def _entries(self): |
340 | - tables = [SnappyDistroSeries, SnappySeries, DistroSeries, Distribution] |
341 | - entries = IStore(self._table).using(*tables).find( |
342 | + entries = IStore(self._table).using(*self._origin).find( |
343 | self._table, *self._clauses) |
344 | return entries.order_by( |
345 | - Distribution.display_name, Desc(DistroSeries.date_created), |
346 | + NullsFirst(Distribution.display_name), |
347 | + Desc(DistroSeries.date_created), |
348 | Desc(SnappySeries.date_created)) |
349 | |
350 | def toTerm(self, obj): |
351 | """See `IVocabulary`.""" |
352 | - token = "%s/%s/%s" % ( |
353 | - obj.distro_series.distribution.name, obj.distro_series.name, |
354 | - obj.snappy_series.name) |
355 | + if obj.distro_series is None: |
356 | + token = obj.snappy_series.name |
357 | + else: |
358 | + token = "%s/%s/%s" % ( |
359 | + obj.distro_series.distribution.name, obj.distro_series.name, |
360 | + obj.snappy_series.name) |
361 | return SimpleTerm(obj, token, obj.title) |
362 | |
363 | def __contains__(self, value): |
364 | @@ -96,15 +115,20 @@ |
365 | |
366 | def getTermByToken(self, token): |
367 | """See `IVocabularyTokenized`.""" |
368 | - try: |
369 | - distribution_name, distro_series_name, snappy_series_name = ( |
370 | - token.split("/", 2)) |
371 | - except ValueError: |
372 | - raise LookupError(token) |
373 | - entry = IStore(self._table).find( |
374 | + if "/" in token: |
375 | + try: |
376 | + distribution_name, distro_series_name, snappy_series_name = ( |
377 | + token.split("/", 2)) |
378 | + except ValueError: |
379 | + raise LookupError(token) |
380 | + else: |
381 | + distribution_name = None |
382 | + distro_series_name = None |
383 | + snappy_series_name = token |
384 | + entry = IStore(self._table).using(*self._origin).find( |
385 | self._table, |
386 | - Distribution.name == distribution_name, |
387 | - DistroSeries.name == distro_series_name, |
388 | + Not(IsDistinctFrom(Distribution.name, distribution_name)), |
389 | + Not(IsDistinctFrom(DistroSeries.name, distro_series_name)), |
390 | SnappySeries.name == snappy_series_name, |
391 | *self._clauses).one() |
392 | if entry is None: |
393 | |
394 | === modified file 'lib/lp/testing/factory.py' |
395 | --- lib/lp/testing/factory.py 2019-02-07 16:35:34 +0000 |
396 | +++ lib/lp/testing/factory.py 2019-02-07 16:35:34 +0000 |
397 | @@ -4824,7 +4824,8 @@ |
398 | def makeSnappySeries(self, registrant=None, name=None, display_name=None, |
399 | status=SeriesStatus.DEVELOPMENT, |
400 | preferred_distro_series=None, date_created=DEFAULT, |
401 | - usable_distro_series=None): |
402 | + usable_distro_series=None, |
403 | + can_infer_distro_series=False): |
404 | """Make a new SnappySeries.""" |
405 | if registrant is None: |
406 | registrant = self.makePerson() |
407 | @@ -4841,6 +4842,8 @@ |
408 | snappy_series.usable_distro_series = usable_distro_series |
409 | elif preferred_distro_series is not None: |
410 | snappy_series.usable_distro_series = [preferred_distro_series] |
411 | + if can_infer_distro_series: |
412 | + snappy_series.can_infer_distro_series = True |
413 | IStore(snappy_series).flush() |
414 | return snappy_series |
415 |