Merge lp:~cjwatson/launchpad/snappy-series-can-guess-distro-series into lp:launchpad

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
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