Merge ~ilasc/launchpad:bug-1818755 into launchpad:master

Proposed by Ioana Lasc
Status: Merged
Approved by: Ioana Lasc
Approved revision: a396d18a104538835513408ba2db911fc1a38260
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ilasc/launchpad:bug-1818755
Merge into: launchpad:master
Diff against target: 519 lines (+316/-51)
7 files modified
lib/lp/snappy/browser/snap.py (+1/-1)
lib/lp/snappy/browser/tests/test_snap.py (+88/-1)
lib/lp/snappy/configure.zcml (+6/-0)
lib/lp/snappy/model/snappyseries.py (+30/-9)
lib/lp/snappy/tests/test_snapvocabularies.py (+92/-0)
lib/lp/snappy/vocabularies.py (+99/-29)
lib/lp/snappy/vocabularies.zcml (+0/-11)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+375224@code.launchpad.net

Commit message

Added a functional Distro Series Radio buttons widget, changes to the Snap properties and a working "Create New Snap" flow.

Description of the change

This is not ready for Merge, created the MP for Colin and I to look at during the 1:1 tomorrow if we have time. Current state for the MP is:

- Working UI displaying the list of Radio Buttons with all Distro Series in Postgres local instance for the Create new Snap screen (+new-snap)
- Working UI+DB persistence of the new Snap in local Postgres when saving the Snap
- Working UI for snaps listing (+snaps) tested with 2 snaps in local Postgres added through new UI
- Many Integration Tests are currently broken on the branch - only tests with local instance of LP & PgAdmin have been performed.

To post a comment you must log in.
~ilasc/launchpad:bug-1818755 updated
8add3c5... by Ioana Lasc

Added fix for Edit Snap View - was breaking for the new Distro Series Radio Widget.

ff40a3c... by Ioana Lasc

Added fix for Update Snap Package action - was breaking with the switch to the new Distro Series only Radio Widget.

a158b61... by Ioana Lasc

Editing vocabulary for Distro Series instead of creating a new one.

239c812... by Ioana Lasc

Altered Vocabulary to display the Store Series in the Edit list for upgrades from value 1 to 2.

3428c06... by Ioana Lasc

Added appropriate queries in the model to fetch the correct list of Bulidable Distro Series.

2aed2c9... by Ioana Lasc

Working code for the String Based Vocabulary approach - needs cleanup.

7ba1cbd... by Ioana Lasc

Small code cleanup.

Revision history for this message
Ioana Lasc (ilasc) wrote :

This is an "kindly asking for help" type of review request.

There is a modified Test Class name and other aesthetic bits (i.e. string manipulation through list indexes) that I am well aware of and can be ignored as they will definitely be addressed when switching to the new Vocabulary - backed by DB which is essentially the part I need help with.

Revision history for this message
Colin Watson (cjwatson) wrote :

So on reflection I think the attempt to use a non-DB-backed vocabulary for this was a serious red herring that I should have steered you away from sooner. I thought it was more of a brief temporary experiment, but it's led you down the path of making quite time-consuming changes all over the place to support it that aren't going to be mergeable. Similarly, hardcoding IDs is never going to be the right thing to do and I think relying on them has perhaps got in the way of better approaches. I hope these suggestions get you back on the right path.

Regarding how to do the presentational side of these changes, particularly the parts in SnapAddView and SnapEditView: as I mentioned before, there are a couple of possible approaches. We do IMO want to keep the approach where store_series and distro_series are only ever edited in combined form in the UI, even if the store_series part of that is mostly vestigial. That means that we need to figure out a way to represent those combinations in a way that's less confusing.

At the moment, store_distro_series is presented using a LaunchpadRadioWidget, which just renders as a list of radio buttons with choices from the vocabulary. That's fine - there doesn't seem to be a particular need to change that, so leave it alone. The vocabulary's terms are "titled": that is, as well as having a value (a SnappyDistroSeries object) and a token (a machine-readable identifier which can be used in HTML forms), they also have a title which is intended for display to humans. The title seems like the right thing to change in this case.

Now, a vocabulary can either make up its own titles, or it can delegate that to something else. If you look at SnappyDistroSeriesVocabulary.toTerm, it returns SimpleTerm(obj, token, obj.title), meaning that it's just using the "title" property of SnappyDistroSeries. That does mean we're leaking presentational details down into the model layer a bit, but that's relatively harmless in this case as IIRC that title isn't used by anything else. So we probably might as well just decide on what each of the choices should look like in the UI and then make SnappyDistroSeries.title return those.

~ilasc/launchpad:bug-1818755 updated
cb62878... by Ioana Lasc

Moved to a DB backed BuildableSnappyDistroSeriesVocabulary.

ffd2234... by Ioana Lasc

Moved to materializing Vocabulary to List approach

Revision history for this message
Ioana Lasc (ilasc) wrote :

This definitely not ready for a full review - a lot of cleanup to be done - but before I spend time going forward with this approach I think it's wise to get a first round of feedback on the materialized list in entries, particularly with focus on the 'toTerm' method in SnappyDistroSeriesVocabulary and '_entries' in BuildableSnappyDistroSeriesVocabulary.

Revision history for this message
Colin Watson (cjwatson) wrote :

I know you said that there was a lot of cleanup to be done, but I'm concerned that you're getting lost in the weeds here, especially because the approach here doesn't seem to match the advice I gave when we last talked about it, and the direction of travel is going to make things very difficult to maintain.

I've tried to be a bit more specific about what I'm expecting in comments here (though I've tried to identify the main problems rather than doing a full line-by-line), but if anything is at all unclear please get in touch with me as soon as possible - no need to wait for a standup meeting or similar.

review: Needs Fixing
~ilasc/launchpad:bug-1818755 updated
91c6498... by Ioana Lasc

Implemented the synthetic SnappyDistroSeries object as a type for the materialized list returned from BuildableSnappyDistroSeriesVocabulary

Revision history for this message
Ioana Lasc (ilasc) wrote :

Unit Tests will take a while to write & fix and before I go down that time-sync I thought I'd do what I said I would and update the MP with code that needs to be on the right path in order to spend time further on implementation.
This code works locally only for displaying a list for Create and Edit Snap (based on Current and Older Store Series).

On the topic of the 4 situations that we want to capture with the new title formatting:
1: Default Case  -> this code currently shows Ubuntu core 16
2: For Current Store Series - > this code currently shows: just the Distro Series
3: For Future Store Series versions -> this code currently shows: Distro Series for Store Series
4: For Older Store Series -> this code currently shows: Distro Series for Store Series

I'm working on trying to come up with something next that will hopefully work well for all 4 situations.

Revision history for this message
Colin Watson (cjwatson) wrote :

This is looking much better, thanks! Some notes follow.

~ilasc/launchpad:bug-1818755 updated
58dc01a... by Ioana Lasc

Added enhancements to BuildableSnappyDistroSeriesVocabulary and a first Unit Test with required changes.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin!

Addressed a few of the previous review comments and added a first Unit Test with a few changes required to be able to not create a second makeSnappySeries() in factory.py - currently breaks a few existing unit tests on the branch.

Updated the MP for early feedback on changes so far in case anything looks out of place.

Still to do:

1. move the check for (context == Snap) to SnappyDistroSeriesVocabulary
2. complete changes for title alterations per Colin's spec
3. Add Unit Tests and fix the currently broken ones.

~ilasc/launchpad:bug-1818755 updated
8cbf47d... by Ioana Lasc

Moved the check for (context == Snap) to SnappyDistroSeriesVocabulary and deleted BuildableSnappyDistroSeriesVocabulary.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Moved the check for (context == Snap) to SnappyDistroSeriesVocabulary and deleted BuildableSnappyDistroSeriesVocabulary. I didn't see the point in keeping the BuildableSnappyDistroSeriesVocabulary - might be wrong but it looked like there's nothing left in it.

Still to do:

2. complete changes for title alterations per Colin's spec
3. Add Unit Tests and fix the currently broken ones.

Revision history for this message
Colin Watson (cjwatson) wrote :

Hmm, I suppose it's probably OK (and indeed less confusing) to delete the Buildable version now that the basic version is more sophisticated, but in that case could you please add the `SnappySeries.status.is_in(ACTIVE_STATUSES)` clause to SnappyDistroSeriesVocabulary? It doesn't make a difference on production at the moment, but in principle it might in the future.

Also if you could clean up import ordering and such (utilities/format-new-and-modified-imports) that would make things easier to read.

Otherwise, getting there, so I've done a bit more of a line-by-line review.

~ilasc/launchpad:bug-1818755 updated
0d0f607... by Ioana Lasc

Adjusted title structure for Snappyseries and moved from a DB OderBy clause to a python list sorting.

7f45f58... by Ioana Lasc

Removed spurious tab from SnappyDistroSeriesVocabulary.toTerm.

Revision history for this message
Ioana Lasc (ilasc) wrote :

MP is now ready for review. Thanks Colin!

Revision history for this message
Colin Watson (cjwatson) wrote :

I suggested earlier that some unit tests for SnappyDistroSeriesVocabulary might be a good idea. I now think that's definitely needed: there are enough special cases in here that it doesn't make sense to try to test for them only in passing in browser-level tests.

Aside from that, I think we're now converging on something that will work, but I've left a number of detailed comments. Thanks.

~ilasc/launchpad:bug-1818755 updated
a453a3d... by Ioana Lasc

Addressed code review comments and added Unit Tests.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin for the comments and suggestions on the last round, hopefully we're now closer now.

It's ready now for another review.

Worked through this list of direct issues:

1 Vocabulary sorting function was crashing on entries where distro_series was None.
2 The sort function used distro_series.display_name rather than distro_series.distribution.display_name
3 Sorting entries in Vocabulary in descending order by date_created - using a convertion to epoch
4 Existing Unit Tests were failing for titles with Store Series None - it was a problem with the Vocabulary code.
5 The bullet list didn't meet the mini spec - was missing the "Infer from snapcraft.yaml (recommended)" entry
6 Title needs to be the same for SnappyDistroSeries and SyntheticSnappyDistroSeries
7 More Unit Tests in Browser
8 Add Vocabulary Unit Tests

Revision history for this message
Colin Watson (cjwatson) wrote :

Thanks for working through all this! This is looking much better now. I've done a line-by-line review with some specific issues, mostly style but with a couple of functional issues; I think you can land this once those are fixed.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

Also, could you update the commit message and description on this MP, which I think date from much earlier versions? The commit message should ideally be more along the lines of https://commit.style/ too.

Revision history for this message
Colin Watson (cjwatson) wrote :

I just noticed that I left an Approve vote claiming that I'd done a line-by-line review, but there's no record of that review to be found. Whoops! I've done my best to reconstruct what I probably would have said.

~ilasc/launchpad:bug-1818755 updated
8971bd4... by Ioana Lasc

Merge branch 'master' into bug-1818755

5b4762b... by Ioana Lasc

Address code review comments

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thank you Colin, MP ready for another look, all comments addressed apart from 1 as not sure why the Unit Test is not failing - I left the details inline as reply to your previous comments - line 379 in this MP.

Revision history for this message
Colin Watson (cjwatson) :
~ilasc/launchpad:bug-1818755 updated
0a68638... by Ioana Lasc

test_edit_snap_built_for_snappy_series_None fails

Revision history for this message
Ioana Lasc (ilasc) wrote :

The test that is failing for this MP is test_edit_snap_built_for_snappy_series_None in
lib/lp/snappy/browser/tests/test_snap.py with:

Traceback (most recent call last):
  File "/home/ilasc/launchpad/launchpad/lib/lp/snappy/browser/tests/test_snap.py", line 847, in test_edit_snap_built_for_snappy_series_None
    self.assertEqual([], find_tags_by_class(browser.contents, "message"))
  File "/home/ilasc/launchpad/launchpad/env/local/lib/python2.7/site-packages/testtools/testcase.py", line 415, in assertEqual
    self.assertThat(observed, matcher, message)
  File "/home/ilasc/launchpad/launchpad/env/local/lib/python2.7/site-packages/testtools/testcase.py", line 502, in assertThat
    raise mismatch_error
testtools.matchers._impl.MismatchError: !=:
reference = []
actual = [<p class="error message">There is 1 error.</p>,
 <div class="message">Constraint not satisfied</div>]

I have seen the same type of failure in the other tests:
test_edit_snap_built_for_distro_series_None
test_edit_snap_built_for_older_store_series

and fixed it by adding:
                 usable_distro_series=[distro_series],

when constructing the snappy_series object.

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

ilasc, the error you are having is caused by the validation on ISnapEditSchema.store_distro_series field.

I had to debug a bit, and I admit that it was kind of random at first, until I could narrow down. Basically, I have put an ipdb at BaseSnapEditView.validate method, saw that `data` was missing `store_distro_series` value, checked that this is a Choice field with `vocabulary='SnappyDistroSeries', required=True`.

With that, I had a quick look on how Choices validates vocabulary (zope.schema._field.Choice._validate), and it just uses a simple `if value not in vocabulary`, meaning that putting a break point at SnappyDistroSeriesVocabulary.__contains__ would probably help me to find out what was going on.

Long story short, the vocabulary's getTermByToken method returns SyntheticSnappyDistroSeries objects in this test, and __contains__ is not prepared for that.

Basically, changing __contains__ and making sure that SyntheticSnappyDistroSeries has proper accessors permissions set on zcml made the test pass:

- Code diff: https://pastebin.canonical.com/p/BfwCJ8Cp9b/
- Test output: https://pastebin.canonical.com/p/w7Rcmph3gN/

This technically solves the issue with this test, but please check if that makes sense for the whole task point of view (if the permission is correct, and we really should accept any SyntheticSnappyDistroSeries as being part of the Vocabulary.

~ilasc/launchpad:bug-1818755 updated
63274c2... by Ioana Lasc

Fix test_edit_snap_built_for_snappy_series_None

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Thiago!
It makes sense to me from a task scope perspective but there might be more complex implications I might be missing in terms of permissions?
Colin this is now ready for another look.

Revision history for this message
Colin Watson (cjwatson) wrote :

Just assuming that any SyntheticSnappyDistroSeries is part of the vocabulary will probably bite us at some point. I suggest implementing an equality test for it instead so that __contains__ can work more naturally, perhaps as in this patch:

  https://paste.ubuntu.com/p/9kVWX5swyG/

~ilasc/launchpad:bug-1818755 updated
bab382b... by Ioana Lasc

Add equality test for SyntheticSnappyDistroSeries

Revision history for this message
Ioana Lasc (ilasc) wrote :

Agreed on above point Colin, your patch makes sens, applied and tested locally.
MP now ready for another look.

Revision history for this message
Colin Watson (cjwatson) wrote :

The problem I've mentioned below in the case where both distro_series and store_series of an existing snap are None needs to be fixed before landing this, but this otherwise looks pretty close. Thanks!

review: Approve
~ilasc/launchpad:bug-1818755 updated
a396d18... by Ioana Lasc

Handle both snappy and distro series being null

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin!
Agreed it definitely needed handling for both Snappy and Distro series being passed in as null.
MP now ready for review.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
2index 643e989..bda9483 100644
3--- a/lib/lp/snappy/browser/snap.py
4+++ b/lib/lp/snappy/browser/snap.py
5@@ -351,7 +351,7 @@ class ISnapEditSchema(Interface):
6 'store_upload',
7 ])
8 store_distro_series = Choice(
9- vocabulary='BuildableSnappyDistroSeries', required=True,
10+ vocabulary='SnappyDistroSeries', required=True,
11 title='Series')
12 vcs = Choice(vocabulary=VCSType, required=True, title='VCS')
13
14diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
15index 149d289..10e82da 100644
16--- a/lib/lp/snappy/browser/tests/test_snap.py
17+++ b/lib/lp/snappy/browser/tests/test_snap.py
18@@ -128,7 +128,8 @@ class TestSnapNavigation(TestCaseWithFactory):
19 def test_snap(self):
20 snap = self.factory.makeSnap()
21 obj, _, _ = test_traverse(
22- "http://launchpad.test/~%s/+snap/%s" % (snap.owner.name, snap.name))
23+ "http://launchpad.test/~%s/+snap/%s" % (
24+ snap.owner.name, snap.name))
25 self.assertEqual(snap, obj)
26
27
28@@ -778,6 +779,92 @@ class TestSnapEditView(BaseTestSnapView):
29 "the store.\nEdit snap package",
30 MatchesTagText(content, "store_upload"))
31
32+ def test_edit_snap_built_for_older_store_series(self):
33+ distro_series = self.factory.makeUbuntuDistroSeries()
34+ with admin_logged_in():
35+ snappy_series = self.factory.makeSnappySeries(
36+ usable_distro_series=[distro_series],
37+ status=SeriesStatus.SUPPORTED)
38+ snap = self.factory.makeSnap(
39+ registrant=self.person, owner=self.person,
40+ distroseries=distro_series,
41+ store_series=snappy_series,
42+ branch=self.factory.makeAnyBranch())
43+ browser = self.getViewBrowser(snap, view_name="+edit", user=snap.owner)
44+ browser.getControl(name="field.store_distro_series").value = (
45+ "ubuntu/%s/%s" % (distro_series.name, snappy_series.name))
46+ browser.getControl("Update snap package").click()
47+
48+ self.assertEqual([], find_tags_by_class(browser.contents, "message"))
49+ login_person(self.person)
50+ self.assertThat(snap, MatchesStructure.byEquality(
51+ distro_series=distro_series,
52+ store_series=snappy_series))
53+
54+ def test_edit_snap_built_for_distro_series_None(self):
55+ with admin_logged_in():
56+ snappy_series = self.factory.makeSnappySeries(
57+ status=SeriesStatus.CURRENT)
58+ snap = self.factory.makeSnap(
59+ registrant=self.person, owner=self.person,
60+ distroseries=None,
61+ store_series=snappy_series)
62+ browser = self.getViewBrowser(snap, user=self.person)
63+ browser.getLink("Edit snap package").click()
64+ browser.getControl(
65+ name="field.store_distro_series").value = (
66+ browser.getControl(
67+ name="field.store_distro_series"
68+ ).options[0].strip())
69+ browser.getControl("Update snap package").click()
70+ self.assertEqual([], find_tags_by_class(browser.contents, "message"))
71+ login_person(self.person)
72+ self.assertThat(snap, MatchesStructure(
73+ distro_series=Is(None),
74+ store_series=Equals(snappy_series)))
75+
76+ def test_edit_snap_built_for_snappy_series_None(self):
77+ distro_series = self.factory.makeUbuntuDistroSeries()
78+
79+ snap = self.factory.makeSnap(
80+ registrant=self.person, owner=self.person,
81+ distroseries=distro_series,
82+ store_series=None)
83+
84+ browser = self.getViewBrowser(snap, view_name="+edit", user=snap.owner)
85+ self.assertIn(
86+ "ubuntu/%s" % distro_series.name,
87+ browser.getControl(name="field.store_distro_series").options)
88+ browser.getControl(
89+ name="field.store_distro_series").value = (
90+ "ubuntu/%s" % distro_series.name)
91+ browser.getControl("Update snap package").click()
92+ self.assertEqual([], find_tags_by_class(browser.contents, "message"))
93+ login_person(self.person)
94+ self.assertThat(snap, MatchesStructure(
95+ distro_series=Equals(distro_series),
96+ store_series=Is(None)))
97+
98+ def test_edit_snap_built_for_distro_snappy_series_None(self):
99+ snap = self.factory.makeSnap(
100+ registrant=self.person, owner=self.person,
101+ distroseries=None,
102+ store_series=None)
103+
104+ browser = self.getViewBrowser(snap, view_name="+edit", user=snap.owner)
105+ self.assertIn(
106+ "(unset)",
107+ browser.getControl(name="field.store_distro_series").options)
108+ browser.getControl(
109+ name="field.store_distro_series").value = '(unset)'
110+ browser.getControl("Update snap package").click()
111+ self.assertEqual([], find_tags_by_class(browser.contents, "message"))
112+
113+ login_person(self.person)
114+ self.assertThat(snap, MatchesStructure(
115+ distro_series=Is(None),
116+ store_series=Is(None)))
117+
118 def test_edit_snap_sets_date_last_modified(self):
119 # Editing a snap package sets the date_last_modified property.
120 date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
121diff --git a/lib/lp/snappy/configure.zcml b/lib/lp/snappy/configure.zcml
122index d062ee1..a16c664 100644
123--- a/lib/lp/snappy/configure.zcml
124+++ b/lib/lp/snappy/configure.zcml
125@@ -127,6 +127,12 @@
126 interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeries" />
127 </class>
128
129+ <!-- SyntheticSnappyDistroSeries -->
130+ <class class="lp.snappy.vocabularies.SyntheticSnappyDistroSeries">
131+ <allow
132+ interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeries" />
133+ </class>
134+
135 <!-- SnappySeriesSet -->
136 <securedutility
137 class="lp.snappy.model.snappyseries.SnappySeriesSet"
138diff --git a/lib/lp/snappy/model/snappyseries.py b/lib/lp/snappy/model/snappyseries.py
139index 2fbbcaf..c60fe3b 100644
140--- a/lib/lp/snappy/model/snappyseries.py
141+++ b/lib/lp/snappy/model/snappyseries.py
142@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
143 __metaclass__ = type
144 __all__ = [
145 'SnappyDistroSeries',
146+ 'SnappyDistroSeriesMixin',
147 'SnappySeries',
148 ]
149
150@@ -164,8 +165,36 @@ class SnappySeries(Storm):
151 get_property_cache(self)._can_infer_distro_series = False
152
153
154+class SnappyDistroSeriesMixin:
155+
156+ @property
157+ def title(self):
158+ # The conditional for SeriesStatus.CURRENT here
159+ # was introduced in 2020 when CURRENT meant 16
160+ # When we need to introduce a new store_series;
161+ # we can initially add it as FUTURE or EXPERIMENTAL
162+ # until we sort out the UI.
163+
164+ if self.distro_series is not None:
165+ if (self.snappy_series is not None and
166+ self.snappy_series.status != SeriesStatus.CURRENT):
167+ return "%s, for %s" % (
168+ self.distro_series.fullseriesname,
169+ self.snappy_series.title)
170+ else:
171+ return self.distro_series.fullseriesname
172+ else:
173+ if self.snappy_series is not None:
174+ if self.snappy_series.status == SeriesStatus.CURRENT:
175+ return "Infer from snapcraft.yaml (recommended)"
176+ else:
177+ return self.snappy_series.title
178+ else:
179+ return "(unset)"
180+
181+
182 @implementer(ISnappyDistroSeries)
183-class SnappyDistroSeries(Storm):
184+class SnappyDistroSeries(Storm, SnappyDistroSeriesMixin):
185 """Link table between `SnappySeries` and `DistroSeries`."""
186
187 __storm_table__ = 'SnappyDistroSeries'
188@@ -186,14 +215,6 @@ class SnappyDistroSeries(Storm):
189 self.distro_series = distro_series
190 self.preferred = preferred
191
192- @property
193- def title(self):
194- if self.distro_series is not None:
195- return "%s, for %s" % (
196- self.distro_series.fullseriesname, self.snappy_series.title)
197- else:
198- return self.snappy_series.title
199-
200
201 @implementer(ISnappySeriesSet)
202 class SnappySeriesSet:
203diff --git a/lib/lp/snappy/tests/test_snapvocabularies.py b/lib/lp/snappy/tests/test_snapvocabularies.py
204new file mode 100644
205index 0000000..22d90c1
206--- /dev/null
207+++ b/lib/lp/snappy/tests/test_snapvocabularies.py
208@@ -0,0 +1,92 @@
209+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
210+# GNU Affero General Public License version 3 (see the file LICENSE).
211+
212+"""Test the snappy vocabularies."""
213+
214+__metaclass__ = type
215+
216+from lp.registry.interfaces.series import SeriesStatus
217+from lp.snappy.vocabularies import SnappyDistroSeriesVocabulary
218+from lp.testing import TestCaseWithFactory
219+from lp.testing.layers import ZopelessDatabaseLayer
220+
221+
222+class TestSnappyDistroSeriesVocabulary(TestCaseWithFactory):
223+ """Test that the SnappyDistroSeriesVocabulary behaves as expected."""
224+
225+ layer = ZopelessDatabaseLayer
226+
227+ def setUp(self):
228+ super(TestSnappyDistroSeriesVocabulary, self).setUp()
229+ self.vocab = SnappyDistroSeriesVocabulary()
230+
231+ def test_getTermByToken(self):
232+ distro_series = self.factory.makeDistroSeries()
233+ snappy_series = self.factory.makeSnappySeries(
234+ usable_distro_series=[distro_series])
235+
236+ term = self.vocab.getTermByToken("%s/%s/%s" % (
237+ distro_series.distribution.name,
238+ distro_series.name,
239+ snappy_series.name
240+ ))
241+ self.assertIsNotNone(term)
242+
243+ def test_term_structure_for_current_store_series(self):
244+ distro_series = self.factory.makeUbuntuDistroSeries()
245+ snappy_series = self.factory.makeSnappySeries(
246+ usable_distro_series=[distro_series],
247+ status=SeriesStatus.CURRENT
248+ )
249+
250+ term = self.vocab.getTermByToken("%s/%s/%s" % (
251+ distro_series.distribution.name,
252+ distro_series.name,
253+ snappy_series.name
254+ ))
255+
256+ self.assertEqual(term.title, distro_series.fullseriesname)
257+
258+ def test_term_structure_for_older_store_series(self):
259+ distro_series = self.factory.makeUbuntuDistroSeries()
260+ snappy_series = self.factory.makeSnappySeries(
261+ usable_distro_series=[distro_series],
262+ status=SeriesStatus.SUPPORTED)
263+
264+ term = self.vocab.getTermByToken("%s/%s/%s" % (
265+ distro_series.distribution.name,
266+ distro_series.name,
267+ snappy_series.name
268+ ))
269+
270+ self.assertEqual(term.title, "%s, for %s" % (
271+ distro_series.fullseriesname,
272+ snappy_series.title))
273+
274+ def test_term_structure_for_distro_series_None(self):
275+ snappy_series = self.factory.makeSnappySeries(
276+ can_infer_distro_series=True)
277+ term = self.vocab.getTermByToken(snappy_series.name)
278+ self.assertEqual(term.title, snappy_series.title)
279+
280+ def test_term_structure_for_infer_from_snapcraft(self):
281+ snappy_series = self.factory.makeSnappySeries(
282+ can_infer_distro_series=True,
283+ status=SeriesStatus.CURRENT)
284+ term = self.vocab.getTermByToken(snappy_series.name)
285+
286+ self.assertEqual(term.title, "Infer from snapcraft.yaml (recommended)")
287+ self.assertEqual(term.token, snappy_series.name)
288+
289+ def test_term_order_for_distro_series_None(self):
290+ # entry with DistroSeries=None should be at the top of
291+ # the entries list in the vocabulary
292+ # in this instance it will also be the most recent one
293+ # which should also be a the top
294+
295+ snappy_series = self.factory.makeSnappySeries(
296+ can_infer_distro_series=True)
297+ entries = self.vocab._entries
298+
299+ self.assertIsNone(entries[0].distro_series)
300+ self.assertEqual(entries[0].title, snappy_series.title)
301diff --git a/lib/lp/snappy/vocabularies.py b/lib/lp/snappy/vocabularies.py
302index 331088a..ea88db2 100644
303--- a/lib/lp/snappy/vocabularies.py
304+++ b/lib/lp/snappy/vocabularies.py
305@@ -1,14 +1,14 @@
306-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the GNU
307+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the GNU
308 # Affero General Public License version 3 (see the file LICENSE).
309
310 """Snappy vocabularies."""
311
312 from __future__ import absolute_import, print_function, unicode_literals
313
314+
315 __metaclass__ = type
316
317 __all__ = [
318- 'BuildableSnappyDistroSeriesVocabulary',
319 'SnapDistroArchSeriesVocabulary',
320 'SnappyDistroSeriesVocabulary',
321 'SnappySeriesVocabulary',
322@@ -34,15 +34,15 @@ from lp.registry.model.distribution import Distribution
323 from lp.registry.model.distroseries import DistroSeries
324 from lp.registry.model.series import ACTIVE_STATUSES
325 from lp.services.database.interfaces import IStore
326-from lp.services.database.stormexpr import (
327- IsDistinctFrom,
328- NullsFirst,
329- )
330+from lp.services.database.stormexpr import IsDistinctFrom
331+from lp.services.utils import seconds_since_epoch
332 from lp.services.webapp.vocabulary import StormVocabularyBase
333 from lp.snappy.interfaces.snap import ISnap
334+from lp.snappy.interfaces.snappyseries import ISnappyDistroSeries
335 from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
336 from lp.snappy.model.snappyseries import (
337 SnappyDistroSeries,
338+ SnappyDistroSeriesMixin,
339 SnappySeries,
340 )
341 from lp.soyuz.model.distroarchseries import DistroArchSeries
342@@ -72,6 +72,45 @@ class SnappySeriesVocabulary(StormVocabularyBase):
343 _order_by = Desc(SnappySeries.date_created)
344
345
346+@implementer(ISnappyDistroSeries)
347+class SyntheticSnappyDistroSeries(SnappyDistroSeriesMixin):
348+ def __init__(self, snappy_series, distro_series):
349+ self.snappy_series = snappy_series
350+ self.distro_series = distro_series
351+
352+ preferred = False
353+
354+ def __eq__(self, other):
355+ return (
356+ ISnappyDistroSeries.providedBy(other) and
357+ self.snappy_series == other.snappy_series and
358+ self.distro_series == other.distro_series)
359+
360+ def __ne__(self, other):
361+ return not (self == other)
362+
363+
364+def sorting_tuple_date_created(element):
365+ # we negate the conversion to epoch here of
366+ # the two date_created in order to achieve
367+ # descending order
368+ if element.distro_series is not None:
369+ if element.snappy_series is not None:
370+ return (1, element.distro_series.distribution.display_name,
371+ (-seconds_since_epoch(element.distro_series.date_created)),
372+ (-seconds_since_epoch(element.snappy_series.date_created)))
373+ else:
374+ return (1, element.distro_series.distribution.display_name,
375+ (-seconds_since_epoch(element.distro_series.date_created)),
376+ None)
377+ else:
378+ if element.snappy_series is not None:
379+ return (0, None, None,
380+ (-seconds_since_epoch(element.snappy_series.date_created)))
381+ else:
382+ return 0, None, None, None
383+
384+
385 class SnappyDistroSeriesVocabulary(StormVocabularyBase):
386 """A vocabulary for searching snappy/distro series combinations."""
387
388@@ -84,25 +123,39 @@ class SnappyDistroSeriesVocabulary(StormVocabularyBase):
389 LeftJoin(Distribution, DistroSeries.distributionID == Distribution.id),
390 SnappySeries,
391 ]
392- _clauses = [SnappyDistroSeries.snappy_series_id == SnappySeries.id]
393+ _clauses = [SnappyDistroSeries.snappy_series_id == SnappySeries.id,
394+ SnappySeries.status.is_in(ACTIVE_STATUSES)]
395
396 @property
397 def _entries(self):
398- entries = IStore(self._table).using(*self._origin).find(
399- self._table, *self._clauses)
400- return entries.order_by(
401- NullsFirst(Distribution.display_name),
402- Desc(DistroSeries.date_created),
403- Desc(SnappySeries.date_created))
404+ entries = list(IStore(self._table).using(*self._origin).find(
405+ self._table, *self._clauses))
406+
407+ if (ISnap.providedBy(self.context) and not
408+ any(entry.snappy_series == self.context.store_series
409+ and entry.distro_series == self.context.distro_series
410+ for entry in entries)):
411+ entries.append(SyntheticSnappyDistroSeries(
412+ self.context.store_series, self.context.distro_series))
413+ return sorted(entries, key=sorting_tuple_date_created)
414
415 def toTerm(self, obj):
416 """See `IVocabulary`."""
417- if obj.distro_series is None:
418- token = obj.snappy_series.name
419+ if obj.snappy_series is not None:
420+ if obj.distro_series is None:
421+ token = obj.snappy_series.name
422+ else:
423+ token = "%s/%s/%s" % (
424+ obj.distro_series.distribution.name,
425+ obj.distro_series.name,
426+ obj.snappy_series.name)
427 else:
428- token = "%s/%s/%s" % (
429- obj.distro_series.distribution.name, obj.distro_series.name,
430- obj.snappy_series.name)
431+ if obj.distro_series is None:
432+ token = "(unset)"
433+ else:
434+ token = "%s/%s" % (
435+ obj.distro_series.distribution.name,
436+ obj.distro_series.name)
437 return SimpleTerm(obj, token, obj.title)
438
439 def __contains__(self, value):
440@@ -117,11 +170,17 @@ class SnappyDistroSeriesVocabulary(StormVocabularyBase):
441
442 def getTermByToken(self, token):
443 """See `IVocabularyTokenized`."""
444+ if token == "(unset)":
445+ return self.toTerm(SyntheticSnappyDistroSeries(None, None))
446 if "/" in token:
447- try:
448+ bits = token.split("/", 2)
449+ if len(bits) == 2:
450+ distribution_name, distro_series_name = bits
451+ snappy_series_name = None
452+ elif len(bits) == 3:
453 distribution_name, distro_series_name, snappy_series_name = (
454- token.split("/", 2))
455- except ValueError:
456+ bits)
457+ else:
458 raise LookupError(token)
459 else:
460 distribution_name = None
461@@ -133,17 +192,28 @@ class SnappyDistroSeriesVocabulary(StormVocabularyBase):
462 Not(IsDistinctFrom(DistroSeries.name, distro_series_name)),
463 SnappySeries.name == snappy_series_name,
464 *self._clauses).one()
465+
466+ if entry is None and ISnap.providedBy(self.context):
467+ if self.context.store_series is None:
468+ context_store_series_name = None
469+ else:
470+ context_store_series_name = self.context.store_series.name
471+ if self.context.distro_series is not None:
472+ context_distribution_name = (
473+ self.context.distro_series.distribution.name)
474+ context_distro_series_name = self.context.distro_series.name
475+ else:
476+ context_distribution_name = None
477+ context_distro_series_name = None
478+ if (context_distribution_name == distribution_name and
479+ context_distro_series_name == distro_series_name and
480+ context_store_series_name == snappy_series_name):
481+ entry = SyntheticSnappyDistroSeries(
482+ self.context.store_series, self.context.distro_series)
483 if entry is None:
484 raise LookupError(token)
485- return self.toTerm(entry)
486
487-
488-class BuildableSnappyDistroSeriesVocabulary(SnappyDistroSeriesVocabulary):
489- """A vocabulary for searching active snappy/distro series combinations."""
490-
491- _clauses = SnappyDistroSeriesVocabulary._clauses + [
492- SnappySeries.status.is_in(ACTIVE_STATUSES),
493- ]
494+ return self.toTerm(entry)
495
496
497 @implementer(IJSONPublishable)
498diff --git a/lib/lp/snappy/vocabularies.zcml b/lib/lp/snappy/vocabularies.zcml
499index 8f34394..4df0102 100644
500--- a/lib/lp/snappy/vocabularies.zcml
501+++ b/lib/lp/snappy/vocabularies.zcml
502@@ -37,17 +37,6 @@
503 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
504 </class>
505
506- <securedutility
507- name="BuildableSnappyDistroSeries"
508- component="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary"
509- provides="zope.schema.interfaces.IVocabularyFactory">
510- <allow interface="zope.schema.interfaces.IVocabularyFactory" />
511- </securedutility>
512-
513- <class class="lp.snappy.vocabularies.BuildableSnappyDistroSeriesVocabulary">
514- <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
515- </class>
516-
517 <class class="lp.snappy.vocabularies.SnapStoreChannel">
518 <allow interface="zope.schema.interfaces.ITitledTokenizedTerm
519 lazr.restful.interfaces.IJSONPublishable" />

Subscribers

People subscribed via source and target branches

to status/vote changes: