Merge ~ilasc/launchpad:bug-1818755 into launchpad:master
- Git
- lp:~ilasc/launchpad
- bug-1818755
- Merge into master
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) |
Related bugs: |
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.
- 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.
Ioana Lasc (ilasc) wrote : | # |
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 LaunchpadRadioW
Now, a vocabulary can either make up its own titles, or it can delegate that to something else. If you look at SnappyDistroSer
- cb62878... by Ioana Lasc
-
Moved to a DB backed BuildableSnappy
DistroSeriesVoc abulary. - ffd2234... by Ioana Lasc
-
Moved to materializing Vocabulary to List approach
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 SnappyDistroSer
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.
- 91c6498... by Ioana Lasc
-
Implemented the synthetic SnappyDistroSeries object as a type for the materialized list returned from BuildableSnappy
DistroSeriesVoc abulary
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.
Colin Watson (cjwatson) wrote : | # |
This is looking much better, thanks! Some notes follow.
- 58dc01a... by Ioana Lasc
-
Added enhancements to BuildableSnappy
DistroSeriesVoc abulary and a first Unit Test with required changes.
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 SnappyDistroSer
2. complete changes for title alterations per Colin's spec
3. Add Unit Tests and fix the currently broken ones.
- 8cbf47d... by Ioana Lasc
-
Moved the check for (context == Snap) to SnappyDistroSer
iesVocabulary and deleted BuildableSnappy DistroSeriesVoc abulary.
Ioana Lasc (ilasc) wrote : | # |
Moved the check for (context == Snap) to SnappyDistroSer
Still to do:
2. complete changes for title alterations per Colin's spec
3. Add Unit Tests and fix the currently broken ones.
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.
Also if you could clean up import ordering and such (utilities/
Otherwise, getting there, so I've done a bit more of a line-by-line review.
- 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 SnappyDistroSer
iesVocabulary. toTerm.
Ioana Lasc (ilasc) wrote : | # |
MP is now ready for review. Thanks Colin!
Colin Watson (cjwatson) wrote : | # |
I suggested earlier that some unit tests for SnappyDistroSer
Aside from that, I think we're now converging on something that will work, but I've left a number of detailed comments. Thanks.
- a453a3d... by Ioana Lasc
-
Addressed code review comments and added Unit Tests.
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_
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 SyntheticSnappy
7 More Unit Tests in Browser
8 Add Vocabulary Unit Tests
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.
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:/
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.
- 8971bd4... by Ioana Lasc
-
Merge branch 'master' into bug-1818755
- 5b4762b... by Ioana Lasc
-
Address code review comments
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.
Colin Watson (cjwatson) : | # |
- 0a68638... by Ioana Lasc
-
test_edit_
snap_built_ for_snappy_ series_ None fails
Ioana Lasc (ilasc) wrote : | # |
The test that is failing for this MP is test_edit_
lib/lp/
Traceback (most recent call last):
File "/home/
self.
File "/home/
self.
File "/home/
raise mismatch_error
testtools.
reference = []
actual = [<p class="error message">There is 1 error.</p>,
<div class="
I have seen the same type of failure in the other tests:
test_edit_
test_edit_
and fixed it by adding:
when constructing the snappy_series object.
Thiago F. Pappacena (pappacena) wrote : | # |
ilasc, the error you are having is caused by the validation on ISnapEditSchema
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 BaseSnapEditVie
With that, I had a quick look on how Choices validates vocabulary (zope.schema.
Long story short, the vocabulary's getTermByToken method returns SyntheticSnappy
Basically, changing __contains__ and making sure that SyntheticSnappy
- Code diff: https:/
- Test output: https:/
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 SyntheticSnappy
- 63274c2... by Ioana Lasc
-
Fix test_edit_
snap_built_ for_snappy_ series_ None
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.
Colin Watson (cjwatson) wrote : | # |
Just assuming that any SyntheticSnappy
- bab382b... by Ioana Lasc
-
Add equality test for SyntheticSnappy
DistroSeries
Ioana Lasc (ilasc) wrote : | # |
Agreed on above point Colin, your patch makes sens, applied and tested locally.
MP now ready for another look.
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!
- a396d18... by Ioana Lasc
-
Handle both snappy and distro series being null
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.
Colin Watson (cjwatson) : | # |
Preview Diff
1 | diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py |
2 | index 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 | |
14 | diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py |
15 | index 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) |
121 | diff --git a/lib/lp/snappy/configure.zcml b/lib/lp/snappy/configure.zcml |
122 | index 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" |
138 | diff --git a/lib/lp/snappy/model/snappyseries.py b/lib/lp/snappy/model/snappyseries.py |
139 | index 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: |
203 | diff --git a/lib/lp/snappy/tests/test_snapvocabularies.py b/lib/lp/snappy/tests/test_snapvocabularies.py |
204 | new file mode 100644 |
205 | index 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) |
301 | diff --git a/lib/lp/snappy/vocabularies.py b/lib/lp/snappy/vocabularies.py |
302 | index 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) |
498 | diff --git a/lib/lp/snappy/vocabularies.zcml b/lib/lp/snappy/vocabularies.zcml |
499 | index 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" /> |
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.