Merge ~cjwatson/launchpad:charm-recipe-create-views into launchpad:master
- Git
- lp:~cjwatson/launchpad
- charm-recipe-create-views
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 3d9db4903f64d9bcbf13ca06def41eaba462a664 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:charm-recipe-create-views |
Merge into: | launchpad:master |
Prerequisite: | ~cjwatson/launchpad:charm-recipe-edit-views |
Diff against target: |
814 lines (+467/-34) 16 files modified
lib/lp/charms/browser/charmrecipe.py (+94/-0) lib/lp/charms/browser/configure.zcml (+13/-0) lib/lp/charms/browser/hascharmrecipes.py (+17/-1) lib/lp/charms/browser/tests/test_charmrecipe.py (+150/-0) lib/lp/charms/browser/tests/test_hascharmrecipes.py (+56/-0) lib/lp/charms/browser/widgets/charmrecipebuildchannels.py (+1/-1) lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt (+4/-4) lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py (+15/-16) lib/lp/charms/interfaces/charmrecipe.py (+2/-2) lib/lp/charms/interfaces/charmrecipebuild.py (+2/-2) lib/lp/charms/interfaces/charmrecipejob.py (+2/-2) lib/lp/charms/templates/charmrecipe-macros.pt (+6/-0) lib/lp/charms/templates/charmrecipe-new.pt (+87/-0) lib/lp/code/browser/gitref.py (+1/-0) lib/lp/registry/browser/product.py (+1/-0) lib/lp/registry/templates/product-index.pt (+16/-6) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+403968@code.launchpad.net |
Commit message
Add views for creating charm recipes
Description of the change
To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py | |||
2 | index 6511424..2345ec1 100644 | |||
3 | --- a/lib/lp/charms/browser/charmrecipe.py | |||
4 | +++ b/lib/lp/charms/browser/charmrecipe.py | |||
5 | @@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals | |||
6 | 7 | 7 | ||
7 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
8 | 9 | __all__ = [ | 9 | __all__ = [ |
9 | 10 | "CharmRecipeAddView", | ||
10 | 10 | "CharmRecipeAdminView", | 11 | "CharmRecipeAdminView", |
11 | 11 | "CharmRecipeDeleteView", | 12 | "CharmRecipeDeleteView", |
12 | 12 | "CharmRecipeEditView", | 13 | "CharmRecipeEditView", |
13 | @@ -30,6 +31,7 @@ from zope.security.interfaces import Unauthorized | |||
14 | 30 | from lp.app.browser.launchpadform import ( | 31 | from lp.app.browser.launchpadform import ( |
15 | 31 | action, | 32 | action, |
16 | 32 | LaunchpadEditFormView, | 33 | LaunchpadEditFormView, |
17 | 34 | LaunchpadFormView, | ||
18 | 33 | ) | 35 | ) |
19 | 34 | from lp.app.browser.lazrjs import InlinePersonEditPickerWidget | 36 | from lp.app.browser.lazrjs import InlinePersonEditPickerWidget |
20 | 35 | from lp.app.browser.tales import format_link | 37 | from lp.app.browser.tales import format_link |
21 | @@ -43,7 +45,9 @@ from lp.charms.interfaces.charmrecipe import ( | |||
22 | 43 | ) | 45 | ) |
23 | 44 | from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet | 46 | from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet |
24 | 45 | from lp.code.browser.widgets.gitref import GitRefWidget | 47 | from lp.code.browser.widgets.gitref import GitRefWidget |
25 | 48 | from lp.code.interfaces.gitref import IGitRef | ||
26 | 46 | from lp.registry.interfaces.personproduct import IPersonProductFactory | 49 | from lp.registry.interfaces.personproduct import IPersonProductFactory |
27 | 50 | from lp.registry.interfaces.product import IProduct | ||
28 | 47 | from lp.services.propertycache import cachedproperty | 51 | from lp.services.propertycache import cachedproperty |
29 | 48 | from lp.services.utils import seconds_since_epoch | 52 | from lp.services.utils import seconds_since_epoch |
30 | 49 | from lp.services.webapp import ( | 53 | from lp.services.webapp import ( |
31 | @@ -240,6 +244,96 @@ class ICharmRecipeEditSchema(Interface): | |||
32 | 240 | store_channels = copy_field(ICharmRecipe["store_channels"], required=True) | 244 | store_channels = copy_field(ICharmRecipe["store_channels"], required=True) |
33 | 241 | 245 | ||
34 | 242 | 246 | ||
35 | 247 | class CharmRecipeAddView(LaunchpadFormView): | ||
36 | 248 | """View for creating charm recipes.""" | ||
37 | 249 | |||
38 | 250 | page_title = label = "Create a new charm recipe" | ||
39 | 251 | |||
40 | 252 | schema = ICharmRecipeEditSchema | ||
41 | 253 | |||
42 | 254 | custom_widget_git_ref = GitRefWidget | ||
43 | 255 | custom_widget_auto_build_channels = CharmRecipeBuildChannelsWidget | ||
44 | 256 | custom_widget_store_channels = StoreChannelsWidget | ||
45 | 257 | |||
46 | 258 | @property | ||
47 | 259 | def field_names(self): | ||
48 | 260 | fields = ["owner", "name"] | ||
49 | 261 | if self.is_project_context: | ||
50 | 262 | fields += ["git_ref"] | ||
51 | 263 | else: | ||
52 | 264 | fields += ["project"] | ||
53 | 265 | return fields + [ | ||
54 | 266 | "auto_build", | ||
55 | 267 | "auto_build_channels", | ||
56 | 268 | "store_upload", | ||
57 | 269 | "store_name", | ||
58 | 270 | "store_channels", | ||
59 | 271 | ] | ||
60 | 272 | |||
61 | 273 | @property | ||
62 | 274 | def is_project_context(self): | ||
63 | 275 | return IProduct.providedBy(self.context) | ||
64 | 276 | |||
65 | 277 | @property | ||
66 | 278 | def cancel_url(self): | ||
67 | 279 | return canonical_url(self.context) | ||
68 | 280 | |||
69 | 281 | @property | ||
70 | 282 | def initial_values(self): | ||
71 | 283 | initial_values = {"owner": self.user} | ||
72 | 284 | if (IGitRef.providedBy(self.context) and | ||
73 | 285 | IProduct.providedBy(self.context.target)): | ||
74 | 286 | initial_values["project"] = self.context.target | ||
75 | 287 | return initial_values | ||
76 | 288 | |||
77 | 289 | def validate_widgets(self, data, names=None): | ||
78 | 290 | """See `LaunchpadFormView`.""" | ||
79 | 291 | if self.widgets.get("store_upload") is not None: | ||
80 | 292 | # Set widgets as required or optional depending on the | ||
81 | 293 | # store_upload field. | ||
82 | 294 | super(CharmRecipeAddView, self).validate_widgets( | ||
83 | 295 | data, ["store_upload"]) | ||
84 | 296 | store_upload = data.get("store_upload", False) | ||
85 | 297 | self.widgets["store_name"].context.required = store_upload | ||
86 | 298 | self.widgets["store_channels"].context.required = store_upload | ||
87 | 299 | super(CharmRecipeAddView, self).validate_widgets(data, names=names) | ||
88 | 300 | |||
89 | 301 | @action("Create charm recipe", name="create") | ||
90 | 302 | def create_action(self, action, data): | ||
91 | 303 | if IGitRef.providedBy(self.context): | ||
92 | 304 | project = data["project"] | ||
93 | 305 | git_ref = self.context | ||
94 | 306 | elif self.is_project_context: | ||
95 | 307 | project = self.context | ||
96 | 308 | git_ref = data["git_ref"] | ||
97 | 309 | else: | ||
98 | 310 | raise NotImplementedError( | ||
99 | 311 | "Unknown context for charm recipe creation.") | ||
100 | 312 | recipe = getUtility(ICharmRecipeSet).new( | ||
101 | 313 | self.user, data["owner"], project, data["name"], git_ref=git_ref, | ||
102 | 314 | auto_build=data["auto_build"], | ||
103 | 315 | auto_build_channels=data["auto_build_channels"], | ||
104 | 316 | store_upload=data["store_upload"], | ||
105 | 317 | store_name=data["store_name"], | ||
106 | 318 | store_channels=data.get("store_channels")) | ||
107 | 319 | self.next_url = canonical_url(recipe) | ||
108 | 320 | |||
109 | 321 | def validate(self, data): | ||
110 | 322 | super(CharmRecipeAddView, self).validate(data) | ||
111 | 323 | owner = data.get("owner", None) | ||
112 | 324 | if self.is_project_context: | ||
113 | 325 | project = self.context | ||
114 | 326 | else: | ||
115 | 327 | project = data.get("project", None) | ||
116 | 328 | name = data.get("name", None) | ||
117 | 329 | if owner and project and name: | ||
118 | 330 | if getUtility(ICharmRecipeSet).exists(owner, project, name): | ||
119 | 331 | self.setFieldError( | ||
120 | 332 | "name", | ||
121 | 333 | "There is already a charm recipe owned by %s in %s with " | ||
122 | 334 | "this name." % (owner.display_name, project.display_name)) | ||
123 | 335 | |||
124 | 336 | |||
125 | 243 | class BaseCharmRecipeEditView(LaunchpadEditFormView): | 337 | class BaseCharmRecipeEditView(LaunchpadEditFormView): |
126 | 244 | 338 | ||
127 | 245 | schema = ICharmRecipeEditSchema | 339 | schema = ICharmRecipeEditSchema |
128 | diff --git a/lib/lp/charms/browser/configure.zcml b/lib/lp/charms/browser/configure.zcml | |||
129 | index 60e3dc4..72be851 100644 | |||
130 | --- a/lib/lp/charms/browser/configure.zcml | |||
131 | +++ b/lib/lp/charms/browser/configure.zcml | |||
132 | @@ -50,6 +50,19 @@ | |||
133 | 50 | factory="lp.charms.browser.charmrecipe.CharmRecipeBreadcrumb" | 50 | factory="lp.charms.browser.charmrecipe.CharmRecipeBreadcrumb" |
134 | 51 | permission="zope.Public" /> | 51 | permission="zope.Public" /> |
135 | 52 | 52 | ||
136 | 53 | <browser:page | ||
137 | 54 | for="lp.code.interfaces.gitref.IGitRef" | ||
138 | 55 | class="lp.charms.browser.charmrecipe.CharmRecipeAddView" | ||
139 | 56 | permission="launchpad.AnyPerson" | ||
140 | 57 | name="+new-charm-recipe" | ||
141 | 58 | template="../templates/charmrecipe-new.pt" /> | ||
142 | 59 | <browser:page | ||
143 | 60 | for="lp.registry.interfaces.product.IProduct" | ||
144 | 61 | class="lp.charms.browser.charmrecipe.CharmRecipeAddView" | ||
145 | 62 | permission="launchpad.AnyPerson" | ||
146 | 63 | name="+new-charm-recipe" | ||
147 | 64 | template="../templates/charmrecipe-new.pt" /> | ||
148 | 65 | |||
149 | 53 | <browser:url | 66 | <browser:url |
150 | 54 | for="lp.charms.interfaces.charmrecipe.ICharmRecipeBuildRequest" | 67 | for="lp.charms.interfaces.charmrecipe.ICharmRecipeBuildRequest" |
151 | 55 | path_expression="string:+build-request/${id}" | 68 | path_expression="string:+build-request/${id}" |
152 | diff --git a/lib/lp/charms/browser/hascharmrecipes.py b/lib/lp/charms/browser/hascharmrecipes.py | |||
153 | index 5c395b5..16f6f4c 100644 | |||
154 | --- a/lib/lp/charms/browser/hascharmrecipes.py | |||
155 | +++ b/lib/lp/charms/browser/hascharmrecipes.py | |||
156 | @@ -13,8 +13,13 @@ __all__ = [ | |||
157 | 13 | 13 | ||
158 | 14 | from zope.component import getUtility | 14 | from zope.component import getUtility |
159 | 15 | 15 | ||
161 | 16 | from lp.charms.interfaces.charmrecipe import ICharmRecipeSet | 16 | from lp.charms.interfaces.charmrecipe import ( |
162 | 17 | CHARM_RECIPE_ALLOW_CREATE, | ||
163 | 18 | CHARM_RECIPE_PRIVATE_FEATURE_FLAG, | ||
164 | 19 | ICharmRecipeSet, | ||
165 | 20 | ) | ||
166 | 17 | from lp.code.interfaces.gitrepository import IGitRepository | 21 | from lp.code.interfaces.gitrepository import IGitRepository |
167 | 22 | from lp.services.features import getFeatureFlag | ||
168 | 18 | from lp.services.webapp import ( | 23 | from lp.services.webapp import ( |
169 | 19 | canonical_url, | 24 | canonical_url, |
170 | 20 | Link, | 25 | Link, |
171 | @@ -31,6 +36,17 @@ class HasCharmRecipesMenuMixin: | |||
172 | 31 | self.context, visible_by_user=self.user).is_empty() | 36 | self.context, visible_by_user=self.user).is_empty() |
173 | 32 | return Link("+charm-recipes", text, icon="info", enabled=enabled) | 37 | return Link("+charm-recipes", text, icon="info", enabled=enabled) |
174 | 33 | 38 | ||
175 | 39 | def create_charm_recipe(self): | ||
176 | 40 | # Only enabled for private contexts if the | ||
177 | 41 | # charm.recipe.allow_private flag is enabled. | ||
178 | 42 | enabled = ( | ||
179 | 43 | bool(getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE)) and ( | ||
180 | 44 | not self.context.private or | ||
181 | 45 | bool(getFeatureFlag(CHARM_RECIPE_PRIVATE_FEATURE_FLAG)))) | ||
182 | 46 | |||
183 | 47 | text = "Create charm recipe" | ||
184 | 48 | return Link("+new-charm-recipe", text, enabled=enabled, icon="add") | ||
185 | 49 | |||
186 | 34 | 50 | ||
187 | 35 | class HasCharmRecipesViewMixin: | 51 | class HasCharmRecipesViewMixin: |
188 | 36 | """A view mixin for objects that have charm recipes.""" | 52 | """A view mixin for objects that have charm recipes.""" |
189 | diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py | |||
190 | index fcc7f7b..c6130cb 100644 | |||
191 | --- a/lib/lp/charms/browser/tests/test_charmrecipe.py | |||
192 | +++ b/lib/lp/charms/browser/tests/test_charmrecipe.py | |||
193 | @@ -66,6 +66,7 @@ from lp.testing.pages import ( | |||
194 | 66 | extract_text, | 66 | extract_text, |
195 | 67 | find_main_content, | 67 | find_main_content, |
196 | 68 | find_tags_by_class, | 68 | find_tags_by_class, |
197 | 69 | find_tag_by_id, | ||
198 | 69 | ) | 70 | ) |
199 | 70 | from lp.testing.publication import test_traverse | 71 | from lp.testing.publication import test_traverse |
200 | 71 | from lp.testing.views import ( | 72 | from lp.testing.views import ( |
201 | @@ -111,6 +112,155 @@ class BaseTestCharmRecipeView(BrowserTestCase): | |||
202 | 111 | name="test-person", displayname="Test Person") | 112 | name="test-person", displayname="Test Person") |
203 | 112 | 113 | ||
204 | 113 | 114 | ||
205 | 115 | class TestCharmRecipeAddView(BaseTestCharmRecipeView): | ||
206 | 116 | |||
207 | 117 | def test_create_new_recipe_not_logged_in(self): | ||
208 | 118 | [git_ref] = self.factory.makeGitRefs() | ||
209 | 119 | self.assertRaises( | ||
210 | 120 | Unauthorized, self.getViewBrowser, git_ref, | ||
211 | 121 | view_name="+new-charm-recipe", no_login=True) | ||
212 | 122 | |||
213 | 123 | def test_create_new_recipe_git(self): | ||
214 | 124 | self.factory.makeProduct( | ||
215 | 125 | name="test-project", displayname="Test Project") | ||
216 | 126 | [git_ref] = self.factory.makeGitRefs( | ||
217 | 127 | owner=self.person, target=self.person) | ||
218 | 128 | source_display = git_ref.display_name | ||
219 | 129 | browser = self.getViewBrowser( | ||
220 | 130 | git_ref, view_name="+new-charm-recipe", user=self.person) | ||
221 | 131 | browser.getControl(name="field.name").value = "charm-name" | ||
222 | 132 | self.assertEqual("", browser.getControl(name="field.project").value) | ||
223 | 133 | browser.getControl(name="field.project").value = "test-project" | ||
224 | 134 | browser.getControl("Create charm recipe").click() | ||
225 | 135 | |||
226 | 136 | content = find_main_content(browser.contents) | ||
227 | 137 | self.assertEqual("charm-name", extract_text(content.h1)) | ||
228 | 138 | self.assertThat( | ||
229 | 139 | "Test Person", MatchesPickerText(content, "edit-owner")) | ||
230 | 140 | self.assertThat( | ||
231 | 141 | "Project:\nTest Project\nEdit charm recipe", | ||
232 | 142 | MatchesTagText(content, "project")) | ||
233 | 143 | self.assertThat( | ||
234 | 144 | "Source:\n%s\nEdit charm recipe" % source_display, | ||
235 | 145 | MatchesTagText(content, "source")) | ||
236 | 146 | self.assertThat( | ||
237 | 147 | "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n", | ||
238 | 148 | MatchesTagText(content, "auto_build")) | ||
239 | 149 | self.assertIsNone(find_tag_by_id(content, "auto_build_channels")) | ||
240 | 150 | self.assertThat( | ||
241 | 151 | "Builds of this charm recipe are not automatically uploaded to " | ||
242 | 152 | "the store.\nEdit charm recipe", | ||
243 | 153 | MatchesTagText(content, "store_upload")) | ||
244 | 154 | |||
245 | 155 | def test_create_new_recipe_git_project_namespace(self): | ||
246 | 156 | # If the Git repository is already in a project namespace, then that | ||
247 | 157 | # project is the default for the new recipe. | ||
248 | 158 | project = self.factory.makeProduct( | ||
249 | 159 | name="test-project", displayname="Test Project") | ||
250 | 160 | [git_ref] = self.factory.makeGitRefs(target=project) | ||
251 | 161 | source_display = git_ref.display_name | ||
252 | 162 | browser = self.getViewBrowser( | ||
253 | 163 | git_ref, view_name="+new-charm-recipe", user=self.person) | ||
254 | 164 | browser.getControl(name="field.name").value = "charm-name" | ||
255 | 165 | self.assertEqual( | ||
256 | 166 | "test-project", browser.getControl(name="field.project").value) | ||
257 | 167 | browser.getControl("Create charm recipe").click() | ||
258 | 168 | |||
259 | 169 | content = find_main_content(browser.contents) | ||
260 | 170 | self.assertEqual("charm-name", extract_text(content.h1)) | ||
261 | 171 | self.assertThat( | ||
262 | 172 | "Test Person", MatchesPickerText(content, "edit-owner")) | ||
263 | 173 | self.assertThat( | ||
264 | 174 | "Project:\nTest Project\nEdit charm recipe", | ||
265 | 175 | MatchesTagText(content, "project")) | ||
266 | 176 | self.assertThat( | ||
267 | 177 | "Source:\n%s\nEdit charm recipe" % source_display, | ||
268 | 178 | MatchesTagText(content, "source")) | ||
269 | 179 | self.assertThat( | ||
270 | 180 | "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n", | ||
271 | 181 | MatchesTagText(content, "auto_build")) | ||
272 | 182 | self.assertIsNone(find_tag_by_id(content, "auto_build_channels")) | ||
273 | 183 | self.assertThat( | ||
274 | 184 | "Builds of this charm recipe are not automatically uploaded to " | ||
275 | 185 | "the store.\nEdit charm recipe", | ||
276 | 186 | MatchesTagText(content, "store_upload")) | ||
277 | 187 | |||
278 | 188 | def test_create_new_recipe_project(self): | ||
279 | 189 | project = self.factory.makeProduct(displayname="Test Project") | ||
280 | 190 | [git_ref] = self.factory.makeGitRefs() | ||
281 | 191 | source_display = git_ref.display_name | ||
282 | 192 | browser = self.getViewBrowser( | ||
283 | 193 | project, view_name="+new-charm-recipe", user=self.person) | ||
284 | 194 | browser.getControl(name="field.name").value = "charm-name" | ||
285 | 195 | browser.getControl(name="field.git_ref.repository").value = ( | ||
286 | 196 | git_ref.repository.shortened_path) | ||
287 | 197 | browser.getControl(name="field.git_ref.path").value = git_ref.path | ||
288 | 198 | browser.getControl("Create charm recipe").click() | ||
289 | 199 | |||
290 | 200 | content = find_main_content(browser.contents) | ||
291 | 201 | self.assertEqual("charm-name", extract_text(content.h1)) | ||
292 | 202 | self.assertThat( | ||
293 | 203 | "Test Person", MatchesPickerText(content, "edit-owner")) | ||
294 | 204 | self.assertThat( | ||
295 | 205 | "Project:\nTest Project\nEdit charm recipe", | ||
296 | 206 | MatchesTagText(content, "project")) | ||
297 | 207 | self.assertThat( | ||
298 | 208 | "Source:\n%s\nEdit charm recipe" % source_display, | ||
299 | 209 | MatchesTagText(content, "source")) | ||
300 | 210 | self.assertThat( | ||
301 | 211 | "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n", | ||
302 | 212 | MatchesTagText(content, "auto_build")) | ||
303 | 213 | self.assertIsNone(find_tag_by_id(content, "auto_build_channels")) | ||
304 | 214 | self.assertThat( | ||
305 | 215 | "Builds of this charm recipe are not automatically uploaded to " | ||
306 | 216 | "the store.\nEdit charm recipe", | ||
307 | 217 | MatchesTagText(content, "store_upload")) | ||
308 | 218 | |||
309 | 219 | def test_create_new_recipe_users_teams_as_owner_options(self): | ||
310 | 220 | # Teams that the user is in are options for the charm recipe owner. | ||
311 | 221 | self.factory.makeTeam( | ||
312 | 222 | name="test-team", displayname="Test Team", members=[self.person]) | ||
313 | 223 | [git_ref] = self.factory.makeGitRefs() | ||
314 | 224 | browser = self.getViewBrowser( | ||
315 | 225 | git_ref, view_name="+new-charm-recipe", user=self.person) | ||
316 | 226 | options = browser.getControl("Owner").displayOptions | ||
317 | 227 | self.assertEqual( | ||
318 | 228 | ["Test Person (test-person)", "Test Team (test-team)"], | ||
319 | 229 | sorted(str(option) for option in options)) | ||
320 | 230 | |||
321 | 231 | def test_create_new_recipe_auto_build(self): | ||
322 | 232 | # Creating a new recipe and asking for it to be automatically built | ||
323 | 233 | # sets all the appropriate fields. | ||
324 | 234 | self.factory.makeProduct( | ||
325 | 235 | name="test-project", displayname="Test Project") | ||
326 | 236 | [git_ref] = self.factory.makeGitRefs() | ||
327 | 237 | browser = self.getViewBrowser( | ||
328 | 238 | git_ref, view_name="+new-charm-recipe", user=self.person) | ||
329 | 239 | browser.getControl(name="field.name").value = "charm-name" | ||
330 | 240 | browser.getControl(name="field.project").value = "test-project" | ||
331 | 241 | browser.getControl( | ||
332 | 242 | "Automatically build when branch changes").selected = True | ||
333 | 243 | browser.getControl( | ||
334 | 244 | name="field.auto_build_channels.charmcraft").value = "edge" | ||
335 | 245 | browser.getControl( | ||
336 | 246 | name="field.auto_build_channels.core").value = "stable" | ||
337 | 247 | browser.getControl( | ||
338 | 248 | name="field.auto_build_channels.core18").value = "beta" | ||
339 | 249 | browser.getControl( | ||
340 | 250 | name="field.auto_build_channels.core20").value = "edge/feature" | ||
341 | 251 | browser.getControl("Create charm recipe").click() | ||
342 | 252 | |||
343 | 253 | content = find_main_content(browser.contents) | ||
344 | 254 | self.assertThat( | ||
345 | 255 | "Build schedule:\n(?)\nBuilt automatically\nEdit charm recipe\n", | ||
346 | 256 | MatchesTagText(content, "auto_build")) | ||
347 | 257 | self.assertThat( | ||
348 | 258 | "Source snap channels for automatic builds:\nEdit charm recipe\n" | ||
349 | 259 | "charmcraft\nedge\ncore\nstable\ncore18\nbeta\n" | ||
350 | 260 | "core20\nedge/feature\n", | ||
351 | 261 | MatchesTagText(content, "auto_build_channels")) | ||
352 | 262 | |||
353 | 263 | |||
354 | 114 | class TestCharmRecipeAdminView(BaseTestCharmRecipeView): | 264 | class TestCharmRecipeAdminView(BaseTestCharmRecipeView): |
355 | 115 | 265 | ||
356 | 116 | def test_unauthorized(self): | 266 | def test_unauthorized(self): |
357 | diff --git a/lib/lp/charms/browser/tests/test_hascharmrecipes.py b/lib/lp/charms/browser/tests/test_hascharmrecipes.py | |||
358 | index 6374893..6cc7bf7 100644 | |||
359 | --- a/lib/lp/charms/browser/tests/test_hascharmrecipes.py | |||
360 | +++ b/lib/lp/charms/browser/tests/test_hascharmrecipes.py | |||
361 | @@ -7,13 +7,16 @@ from __future__ import absolute_import, print_function, unicode_literals | |||
362 | 7 | 7 | ||
363 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
364 | 9 | 9 | ||
365 | 10 | import soupmatchers | ||
366 | 10 | from testscenarios import ( | 11 | from testscenarios import ( |
367 | 11 | load_tests_apply_scenarios, | 12 | load_tests_apply_scenarios, |
368 | 12 | WithScenarios, | 13 | WithScenarios, |
369 | 13 | ) | 14 | ) |
370 | 15 | from testtools.matchers import Not | ||
371 | 14 | 16 | ||
372 | 15 | from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE | 17 | from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE |
373 | 16 | from lp.code.interfaces.gitrepository import IGitRepository | 18 | from lp.code.interfaces.gitrepository import IGitRepository |
374 | 19 | from lp.code.tests.helpers import GitHostingFixture | ||
375 | 17 | from lp.services.features.testing import FeatureFixture | 20 | from lp.services.features.testing import FeatureFixture |
376 | 18 | from lp.services.webapp import canonical_url | 21 | from lp.services.webapp import canonical_url |
377 | 19 | from lp.testing import TestCaseWithFactory | 22 | from lp.testing import TestCaseWithFactory |
378 | @@ -83,4 +86,57 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory): | |||
379 | 83 | self.assertEqual(expected_link, view.charm_recipes_link) | 86 | self.assertEqual(expected_link, view.charm_recipes_link) |
380 | 84 | 87 | ||
381 | 85 | 88 | ||
382 | 89 | class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory): | ||
383 | 90 | |||
384 | 91 | layer = DatabaseFunctionalLayer | ||
385 | 92 | |||
386 | 93 | scenarios = [ | ||
387 | 94 | ("GitRef", {"context_factory": make_git_ref}), | ||
388 | 95 | ] | ||
389 | 96 | |||
390 | 97 | def setUp(self): | ||
391 | 98 | super(TestHasCharmRecipesMenu, self).setUp() | ||
392 | 99 | self.useFixture(GitHostingFixture()) | ||
393 | 100 | |||
394 | 101 | def makeCharmRecipe(self, context): | ||
395 | 102 | return self.factory.makeCharmRecipe(git_ref=context) | ||
396 | 103 | |||
397 | 104 | def test_feature_flag_disabled(self): | ||
398 | 105 | # If the feature flag to allow charm recipe creation is disabled, we | ||
399 | 106 | # don't show a creation link. | ||
400 | 107 | context = self.context_factory(self) | ||
401 | 108 | view = create_initialized_view(context, "+index") | ||
402 | 109 | new_charm_recipe_url = canonical_url( | ||
403 | 110 | context, view_name="+new-charm-recipe") | ||
404 | 111 | self.assertThat(view(), Not(soupmatchers.HTMLContains( | ||
405 | 112 | soupmatchers.Tag( | ||
406 | 113 | "creation link", "a", attrs={"href": new_charm_recipe_url}, | ||
407 | 114 | text="Create charm recipe")))) | ||
408 | 115 | |||
409 | 116 | def test_creation_link_no_recipes(self): | ||
410 | 117 | # An object with no charm recipes shows a creation link. | ||
411 | 118 | self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"})) | ||
412 | 119 | context = self.context_factory(self) | ||
413 | 120 | view = create_initialized_view(context, "+index") | ||
414 | 121 | new_charm_recipe_url = canonical_url( | ||
415 | 122 | context, view_name="+new-charm-recipe") | ||
416 | 123 | self.assertThat(view(), soupmatchers.HTMLContains( | ||
417 | 124 | soupmatchers.Tag( | ||
418 | 125 | "creation link", "a", attrs={"href": new_charm_recipe_url}, | ||
419 | 126 | text="Create charm recipe"))) | ||
420 | 127 | |||
421 | 128 | def test_creation_link_recipes(self): | ||
422 | 129 | # An object with charm recipes shows a creation link. | ||
423 | 130 | self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"})) | ||
424 | 131 | context = self.context_factory(self) | ||
425 | 132 | self.makeCharmRecipe(context) | ||
426 | 133 | view = create_initialized_view(context, "+index") | ||
427 | 134 | new_charm_recipe_url = canonical_url( | ||
428 | 135 | context, view_name="+new-charm-recipe") | ||
429 | 136 | self.assertThat(view(), soupmatchers.HTMLContains( | ||
430 | 137 | soupmatchers.Tag( | ||
431 | 138 | "creation link", "a", attrs={"href": new_charm_recipe_url}, | ||
432 | 139 | text="Create charm recipe"))) | ||
433 | 140 | |||
434 | 141 | |||
435 | 86 | load_tests = load_tests_apply_scenarios | 142 | load_tests = load_tests_apply_scenarios |
436 | diff --git a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py | |||
437 | index ef76294..b1512ed 100644 | |||
438 | --- a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py | |||
439 | +++ b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py | |||
440 | @@ -34,7 +34,7 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget): | |||
441 | 34 | 34 | ||
442 | 35 | template = ViewPageTemplateFile("templates/charmrecipebuildchannels.pt") | 35 | template = ViewPageTemplateFile("templates/charmrecipebuildchannels.pt") |
443 | 36 | hint = False | 36 | hint = False |
445 | 37 | snap_names = ["core", "core18", "core20", "charmcraft"] | 37 | snap_names = ["charmcraft", "core", "core18", "core20"] |
446 | 38 | _widgets_set_up = False | 38 | _widgets_set_up = False |
447 | 39 | 39 | ||
448 | 40 | def __init__(self, context, request): | 40 | def __init__(self, context, request): |
449 | diff --git a/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt b/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt | |||
450 | index cf0052a..0430f4a 100644 | |||
451 | --- a/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt | |||
452 | +++ b/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt | |||
453 | @@ -4,6 +4,10 @@ | |||
454 | 4 | 4 | ||
455 | 5 | <table class="subordinate"> | 5 | <table class="subordinate"> |
456 | 6 | <tr> | 6 | <tr> |
457 | 7 | <td>charmcraft</td> | ||
458 | 8 | <td><div tal:content="structure view/charmcraft_widget" /></td> | ||
459 | 9 | </tr> | ||
460 | 10 | <tr> | ||
461 | 7 | <td>core</td> | 11 | <td>core</td> |
462 | 8 | <td><div tal:content="structure view/core_widget" /></td> | 12 | <td><div tal:content="structure view/core_widget" /></td> |
463 | 9 | </tr> | 13 | </tr> |
464 | @@ -15,10 +19,6 @@ | |||
465 | 15 | <td>core20</td> | 19 | <td>core20</td> |
466 | 16 | <td><div tal:content="structure view/core20_widget" /></td> | 20 | <td><div tal:content="structure view/core20_widget" /></td> |
467 | 17 | </tr> | 21 | </tr> |
468 | 18 | <tr> | ||
469 | 19 | <td>charmcraft</td> | ||
470 | 20 | <td><div tal:content="structure view/charmcraft_widget" /></td> | ||
471 | 21 | </tr> | ||
472 | 22 | </table> | 22 | </table> |
473 | 23 | 23 | ||
474 | 24 | </tal:root> | 24 | </tal:root> |
475 | diff --git a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py | |||
476 | index 6ab1907..67b2040 100644 | |||
477 | --- a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py | |||
478 | +++ b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py | |||
479 | @@ -62,53 +62,53 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory): | |||
480 | 62 | # The subwidgets are set up and a flag is set. | 62 | # The subwidgets are set up and a flag is set. |
481 | 63 | self.widget.setUpSubWidgets() | 63 | self.widget.setUpSubWidgets() |
482 | 64 | self.assertTrue(self.widget._widgets_set_up) | 64 | self.assertTrue(self.widget._widgets_set_up) |
483 | 65 | self.assertIsNotNone(getattr(self.widget, "charmcraft_widget", None)) | ||
484 | 65 | self.assertIsNotNone(getattr(self.widget, "core_widget", None)) | 66 | self.assertIsNotNone(getattr(self.widget, "core_widget", None)) |
485 | 66 | self.assertIsNotNone(getattr(self.widget, "core18_widget", None)) | 67 | self.assertIsNotNone(getattr(self.widget, "core18_widget", None)) |
486 | 67 | self.assertIsNotNone(getattr(self.widget, "core20_widget", None)) | 68 | self.assertIsNotNone(getattr(self.widget, "core20_widget", None)) |
487 | 68 | self.assertIsNotNone(getattr(self.widget, "charmcraft_widget", None)) | ||
488 | 69 | 69 | ||
489 | 70 | def test_setUpSubWidgets_second_call(self): | 70 | def test_setUpSubWidgets_second_call(self): |
490 | 71 | # The setUpSubWidgets method exits early if a flag is set to | 71 | # The setUpSubWidgets method exits early if a flag is set to |
491 | 72 | # indicate that the widgets were set up. | 72 | # indicate that the widgets were set up. |
492 | 73 | self.widget._widgets_set_up = True | 73 | self.widget._widgets_set_up = True |
493 | 74 | self.widget.setUpSubWidgets() | 74 | self.widget.setUpSubWidgets() |
494 | 75 | self.assertIsNone(getattr(self.widget, "charmcraft_widget", None)) | ||
495 | 75 | self.assertIsNone(getattr(self.widget, "core_widget", None)) | 76 | self.assertIsNone(getattr(self.widget, "core_widget", None)) |
496 | 76 | self.assertIsNone(getattr(self.widget, "core18_widget", None)) | 77 | self.assertIsNone(getattr(self.widget, "core18_widget", None)) |
497 | 77 | self.assertIsNone(getattr(self.widget, "core20_widget", None)) | 78 | self.assertIsNone(getattr(self.widget, "core20_widget", None)) |
498 | 78 | self.assertIsNone(getattr(self.widget, "charmcraft_widget", None)) | ||
499 | 79 | 79 | ||
500 | 80 | def test_setRenderedValue_None(self): | 80 | def test_setRenderedValue_None(self): |
501 | 81 | self.widget.setRenderedValue(None) | 81 | self.widget.setRenderedValue(None) |
502 | 82 | self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue()) | ||
503 | 82 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) | 83 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) |
504 | 83 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) | 84 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) |
505 | 84 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) | 85 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) |
506 | 85 | self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue()) | ||
507 | 86 | 86 | ||
508 | 87 | def test_setRenderedValue_empty(self): | 87 | def test_setRenderedValue_empty(self): |
509 | 88 | self.widget.setRenderedValue({}) | 88 | self.widget.setRenderedValue({}) |
510 | 89 | self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue()) | ||
511 | 89 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) | 90 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) |
512 | 90 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) | 91 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) |
513 | 91 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) | 92 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) |
514 | 92 | self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue()) | ||
515 | 93 | 93 | ||
516 | 94 | def test_setRenderedValue_one_channel(self): | 94 | def test_setRenderedValue_one_channel(self): |
517 | 95 | self.widget.setRenderedValue({"charmcraft": "stable"}) | 95 | self.widget.setRenderedValue({"charmcraft": "stable"}) |
518 | 96 | self.assertEqual( | ||
519 | 97 | "stable", self.widget.charmcraft_widget._getCurrentValue()) | ||
520 | 96 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) | 98 | self.assertIsNone(self.widget.core_widget._getCurrentValue()) |
521 | 97 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) | 99 | self.assertIsNone(self.widget.core18_widget._getCurrentValue()) |
522 | 98 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) | 100 | self.assertIsNone(self.widget.core20_widget._getCurrentValue()) |
523 | 99 | self.assertEqual( | ||
524 | 100 | "stable", self.widget.charmcraft_widget._getCurrentValue()) | ||
525 | 101 | 101 | ||
526 | 102 | def test_setRenderedValue_all_channels(self): | 102 | def test_setRenderedValue_all_channels(self): |
527 | 103 | self.widget.setRenderedValue( | 103 | self.widget.setRenderedValue( |
530 | 104 | {"core": "candidate", "core18": "beta", "core20": "edge", | 104 | {"charmcraft": "stable", "core": "candidate", "core18": "beta", |
531 | 105 | "charmcraft": "stable"}) | 105 | "core20": "edge"}) |
532 | 106 | self.assertEqual( | ||
533 | 107 | "stable", self.widget.charmcraft_widget._getCurrentValue()) | ||
534 | 106 | self.assertEqual( | 108 | self.assertEqual( |
535 | 107 | "candidate", self.widget.core_widget._getCurrentValue()) | 109 | "candidate", self.widget.core_widget._getCurrentValue()) |
536 | 108 | self.assertEqual("beta", self.widget.core18_widget._getCurrentValue()) | 110 | self.assertEqual("beta", self.widget.core18_widget._getCurrentValue()) |
537 | 109 | self.assertEqual("edge", self.widget.core20_widget._getCurrentValue()) | 111 | self.assertEqual("edge", self.widget.core20_widget._getCurrentValue()) |
538 | 110 | self.assertEqual( | ||
539 | 111 | "stable", self.widget.charmcraft_widget._getCurrentValue()) | ||
540 | 112 | 112 | ||
541 | 113 | def test_hasInput_false(self): | 113 | def test_hasInput_false(self): |
542 | 114 | # hasInput is false when there are no channels in the form data. | 114 | # hasInput is false when there are no channels in the form data. |
543 | @@ -126,41 +126,40 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory): | |||
544 | 126 | # (At the moment, individual channel names are not validated, so | 126 | # (At the moment, individual channel names are not validated, so |
545 | 127 | # there is no "false" counterpart to this test.) | 127 | # there is no "false" counterpart to this test.) |
546 | 128 | form = { | 128 | form = { |
547 | 129 | "field.auto_build_channels.charmcraft": "stable", | ||
548 | 129 | "field.auto_build_channels.core": "", | 130 | "field.auto_build_channels.core": "", |
549 | 130 | "field.auto_build_channels.core18": "beta", | 131 | "field.auto_build_channels.core18": "beta", |
550 | 131 | "field.auto_build_channels.core20": "edge", | 132 | "field.auto_build_channels.core20": "edge", |
551 | 132 | "field.auto_build_channels.charmcraft": "stable", | ||
552 | 133 | } | 133 | } |
553 | 134 | self.widget.request = LaunchpadTestRequest(form=form) | 134 | self.widget.request = LaunchpadTestRequest(form=form) |
554 | 135 | self.assertTrue(self.widget.hasValidInput()) | 135 | self.assertTrue(self.widget.hasValidInput()) |
555 | 136 | 136 | ||
556 | 137 | def test_getInputValue(self): | 137 | def test_getInputValue(self): |
557 | 138 | form = { | 138 | form = { |
558 | 139 | "field.auto_build_channels.charmcraft": "stable", | ||
559 | 139 | "field.auto_build_channels.core": "", | 140 | "field.auto_build_channels.core": "", |
560 | 140 | "field.auto_build_channels.core18": "beta", | 141 | "field.auto_build_channels.core18": "beta", |
561 | 141 | "field.auto_build_channels.core20": "edge", | 142 | "field.auto_build_channels.core20": "edge", |
562 | 142 | "field.auto_build_channels.charmcraft": "stable", | ||
563 | 143 | } | 143 | } |
564 | 144 | self.widget.request = LaunchpadTestRequest(form=form) | 144 | self.widget.request = LaunchpadTestRequest(form=form) |
565 | 145 | self.assertEqual( | 145 | self.assertEqual( |
568 | 146 | {"core18": "beta", "core20": "edge", | 146 | {"charmcraft": "stable", "core18": "beta", "core20": "edge"}, |
567 | 147 | "charmcraft": "stable"}, | ||
569 | 148 | self.widget.getInputValue()) | 147 | self.widget.getInputValue()) |
570 | 149 | 148 | ||
571 | 150 | def test_call(self): | 149 | def test_call(self): |
572 | 151 | # The __call__ method sets up the widgets. | 150 | # The __call__ method sets up the widgets. |
573 | 152 | markup = self.widget() | 151 | markup = self.widget() |
574 | 152 | self.assertIsNotNone(self.widget.charmcraft_widget) | ||
575 | 153 | self.assertIsNotNone(self.widget.core_widget) | 153 | self.assertIsNotNone(self.widget.core_widget) |
576 | 154 | self.assertIsNotNone(self.widget.core18_widget) | 154 | self.assertIsNotNone(self.widget.core18_widget) |
577 | 155 | self.assertIsNotNone(self.widget.core20_widget) | 155 | self.assertIsNotNone(self.widget.core20_widget) |
578 | 156 | self.assertIsNotNone(self.widget.charmcraft_widget) | ||
579 | 157 | soup = BeautifulSoup(markup) | 156 | soup = BeautifulSoup(markup) |
580 | 158 | fields = soup.find_all(["input"], {"id": re.compile(".*")}) | 157 | fields = soup.find_all(["input"], {"id": re.compile(".*")}) |
581 | 159 | expected_ids = [ | 158 | expected_ids = [ |
582 | 159 | "field.auto_build_channels.charmcraft", | ||
583 | 160 | "field.auto_build_channels.core", | 160 | "field.auto_build_channels.core", |
584 | 161 | "field.auto_build_channels.core18", | 161 | "field.auto_build_channels.core18", |
585 | 162 | "field.auto_build_channels.core20", | 162 | "field.auto_build_channels.core20", |
586 | 163 | "field.auto_build_channels.charmcraft", | ||
587 | 164 | ] | 163 | ] |
588 | 165 | ids = [field["id"] for field in fields] | 164 | ids = [field["id"] for field in fields] |
589 | 166 | self.assertContentEqual(expected_ids, ids) | 165 | self.assertContentEqual(expected_ids, ids) |
590 | diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py | |||
591 | index 9c76cf4..80e4572 100644 | |||
592 | --- a/lib/lp/charms/interfaces/charmrecipe.py | |||
593 | +++ b/lib/lp/charms/interfaces/charmrecipe.py | |||
594 | @@ -445,8 +445,8 @@ class ICharmRecipeEditableAttributes(Interface): | |||
595 | 445 | key_type=TextLine(), required=False, readonly=False, | 445 | key_type=TextLine(), required=False, readonly=False, |
596 | 446 | description=_( | 446 | description=_( |
597 | 447 | "A dictionary mapping snap names to channels to use when building " | 447 | "A dictionary mapping snap names to channels to use when building " |
600 | 448 | "this charm recipe. Currently only 'core', 'core18', 'core20', " | 448 | "this charm recipe. Currently only 'charmcraft', 'core', " |
601 | 449 | "and 'charmcraft' keys are supported.")) | 449 | "'core18', and 'core20' keys are supported.")) |
602 | 450 | 450 | ||
603 | 451 | is_stale = Bool( | 451 | is_stale = Bool( |
604 | 452 | title=_("Charm recipe is stale and is due to be rebuilt."), | 452 | title=_("Charm recipe is stale and is due to be rebuilt."), |
605 | diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py | |||
606 | index 284566f..077ccce 100644 | |||
607 | --- a/lib/lp/charms/interfaces/charmrecipebuild.py | |||
608 | +++ b/lib/lp/charms/interfaces/charmrecipebuild.py | |||
609 | @@ -65,8 +65,8 @@ class ICharmRecipeBuildView(IPackageBuild): | |||
610 | 65 | title=_("Source snap channels to use for this build."), | 65 | title=_("Source snap channels to use for this build."), |
611 | 66 | description=_( | 66 | description=_( |
612 | 67 | "A dictionary mapping snap names to channels to use for this " | 67 | "A dictionary mapping snap names to channels to use for this " |
615 | 68 | "build. Currently only 'core', 'core18', 'core20', " | 68 | "build. Currently only 'charmcraft', 'core', 'core18', and " |
616 | 69 | "and 'charmcraft' keys are supported."), | 69 | "'core20' keys are supported."), |
617 | 70 | key_type=TextLine()) | 70 | key_type=TextLine()) |
618 | 71 | 71 | ||
619 | 72 | virtualized = Bool( | 72 | virtualized = Bool( |
620 | diff --git a/lib/lp/charms/interfaces/charmrecipejob.py b/lib/lp/charms/interfaces/charmrecipejob.py | |||
621 | index 9bdc9e5..1094e5a 100644 | |||
622 | --- a/lib/lp/charms/interfaces/charmrecipejob.py | |||
623 | +++ b/lib/lp/charms/interfaces/charmrecipejob.py | |||
624 | @@ -64,8 +64,8 @@ class ICharmRecipeRequestBuildsJob(IRunnableJob): | |||
625 | 64 | title=_("Source snap channels to use for these builds."), | 64 | title=_("Source snap channels to use for these builds."), |
626 | 65 | description=_( | 65 | description=_( |
627 | 66 | "A dictionary mapping snap names to channels to use for these " | 66 | "A dictionary mapping snap names to channels to use for these " |
630 | 67 | "builds. Currently only 'core', 'core18', 'core20', and " | 67 | "builds. Currently only 'charmcraft', 'core', 'core18', and " |
631 | 68 | "'charmcraft' keys are supported."), | 68 | "'core20' keys are supported."), |
632 | 69 | key_type=TextLine(), required=False, readonly=True) | 69 | key_type=TextLine(), required=False, readonly=True) |
633 | 70 | 70 | ||
634 | 71 | architectures = Set( | 71 | architectures = Set( |
635 | diff --git a/lib/lp/charms/templates/charmrecipe-macros.pt b/lib/lp/charms/templates/charmrecipe-macros.pt | |||
636 | index 93b4d69..fe314e1 100644 | |||
637 | --- a/lib/lp/charms/templates/charmrecipe-macros.pt | |||
638 | +++ b/lib/lp/charms/templates/charmrecipe-macros.pt | |||
639 | @@ -17,6 +17,12 @@ | |||
640 | 17 | </div> | 17 | </div> |
641 | 18 | </div> | 18 | </div> |
642 | 19 | 19 | ||
643 | 20 | <span | ||
644 | 21 | tal:define="link context_menu/create_charm_recipe|nothing" | ||
645 | 22 | tal:condition="python: link and link.enabled" | ||
646 | 23 | tal:replace="structure link/render" | ||
647 | 24 | /> | ||
648 | 25 | |||
649 | 20 | </div> | 26 | </div> |
650 | 21 | 27 | ||
651 | 22 | </tal:root> | 28 | </tal:root> |
652 | diff --git a/lib/lp/charms/templates/charmrecipe-new.pt b/lib/lp/charms/templates/charmrecipe-new.pt | |||
653 | 23 | new file mode 100644 | 29 | new file mode 100644 |
654 | index 0000000..bde870d | |||
655 | --- /dev/null | |||
656 | +++ b/lib/lp/charms/templates/charmrecipe-new.pt | |||
657 | @@ -0,0 +1,87 @@ | |||
658 | 1 | <html | ||
659 | 2 | xmlns="http://www.w3.org/1999/xhtml" | ||
660 | 3 | xmlns:tal="http://xml.zope.org/namespaces/tal" | ||
661 | 4 | xmlns:metal="http://xml.zope.org/namespaces/metal" | ||
662 | 5 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" | ||
663 | 6 | metal:use-macro="view/macro:page/main_side" | ||
664 | 7 | i18n:domain="launchpad"> | ||
665 | 8 | <body> | ||
666 | 9 | |||
667 | 10 | <div metal:fill-slot="main"> | ||
668 | 11 | <div> | ||
669 | 12 | <p> | ||
670 | 13 | A <a href="https://juju.is/docs">charmed operator</a> (packaged as a | ||
671 | 14 | "charm") encapsulates a single application and all the code and | ||
672 | 15 | know-how it takes to operate it, such as how to combine and work with | ||
673 | 16 | other related applications or how to upgrade it. Launchpad can build | ||
674 | 17 | charms using <a href="https://juju.is/docs/sdk">charmcraft</a>, from | ||
675 | 18 | any Git branch on Launchpad that has a <tt>charmcraft.yaml</tt> file. | ||
676 | 19 | </p> | ||
677 | 20 | </div> | ||
678 | 21 | |||
679 | 22 | <div metal:use-macro="context/@@launchpad_form/form"> | ||
680 | 23 | <metal:formbody fill-slot="widgets"> | ||
681 | 24 | <table class="form"> | ||
682 | 25 | <tal:widget define="widget nocall:view/widgets/name"> | ||
683 | 26 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
684 | 27 | </tal:widget> | ||
685 | 28 | <tal:widget define="widget nocall:view/widgets/owner"> | ||
686 | 29 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
687 | 30 | </tal:widget> | ||
688 | 31 | |||
689 | 32 | <tal:not-project-context condition="not: view/is_project_context"> | ||
690 | 33 | <tal:widget define="widget nocall:view/widgets/project"> | ||
691 | 34 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
692 | 35 | </tal:widget> | ||
693 | 36 | </tal:not-project-context> | ||
694 | 37 | |||
695 | 38 | <tal:project-context condition="view/is_project_context"> | ||
696 | 39 | <tal:widget define="widget nocall:view/widgets/git_ref"> | ||
697 | 40 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
698 | 41 | </tal:widget> | ||
699 | 42 | </tal:project-context> | ||
700 | 43 | |||
701 | 44 | <tal:widget define="widget nocall:view/widgets/auto_build"> | ||
702 | 45 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
703 | 46 | </tal:widget> | ||
704 | 47 | <tr> | ||
705 | 48 | <td> | ||
706 | 49 | <table class="subordinate"> | ||
707 | 50 | <tal:widget define="widget nocall:view/widgets/auto_build_channels"> | ||
708 | 51 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
709 | 52 | </tal:widget> | ||
710 | 53 | </table> | ||
711 | 54 | </td> | ||
712 | 55 | </tr> | ||
713 | 56 | |||
714 | 57 | <tal:widget define="widget nocall:view/widgets/store_upload"> | ||
715 | 58 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
716 | 59 | </tal:widget> | ||
717 | 60 | <tr> | ||
718 | 61 | <td> | ||
719 | 62 | <table class="subordinate"> | ||
720 | 63 | <tal:widget define="widget nocall:view/widgets/store_name"> | ||
721 | 64 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
722 | 65 | </tal:widget> | ||
723 | 66 | <tal:widget define="widget nocall:view/widgets/store_channels" | ||
724 | 67 | condition="widget/has_risks_vocabulary"> | ||
725 | 68 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
726 | 69 | </tal:widget> | ||
727 | 70 | </table> | ||
728 | 71 | </td> | ||
729 | 72 | </tr> | ||
730 | 73 | </table> | ||
731 | 74 | </metal:formbody> | ||
732 | 75 | </div> | ||
733 | 76 | |||
734 | 77 | <script type="text/javascript"> | ||
735 | 78 | LPJS.use('lp.charms.charmrecipe.edit', function(Y) { | ||
736 | 79 | Y.on('domready', function(e) { | ||
737 | 80 | Y.lp.charms.charmrecipe.edit.setup(); | ||
738 | 81 | }, window); | ||
739 | 82 | }); | ||
740 | 83 | </script> | ||
741 | 84 | </div> | ||
742 | 85 | |||
743 | 86 | </body> | ||
744 | 87 | </html> | ||
745 | diff --git a/lib/lp/code/browser/gitref.py b/lib/lp/code/browser/gitref.py | |||
746 | index c53f8f1..95abbd9 100644 | |||
747 | --- a/lib/lp/code/browser/gitref.py | |||
748 | +++ b/lib/lp/code/browser/gitref.py | |||
749 | @@ -84,6 +84,7 @@ class GitRefContextMenu( | |||
750 | 84 | facet = 'branches' | 84 | facet = 'branches' |
751 | 85 | links = [ | 85 | links = [ |
752 | 86 | 'browse_commits', | 86 | 'browse_commits', |
753 | 87 | 'create_charm_recipe', | ||
754 | 87 | 'create_recipe', | 88 | 'create_recipe', |
755 | 88 | 'create_snap', | 89 | 'create_snap', |
756 | 89 | 'register_merge', | 90 | 'register_merge', |
757 | diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py | |||
758 | index 628fe0e..63724fd 100644 | |||
759 | --- a/lib/lp/registry/browser/product.py | |||
760 | +++ b/lib/lp/registry/browser/product.py | |||
761 | @@ -588,6 +588,7 @@ class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin, | |||
762 | 588 | 'view_charm_recipes', | 588 | 'view_charm_recipes', |
763 | 589 | 'view_recipes', | 589 | 'view_recipes', |
764 | 590 | 'view_snaps', | 590 | 'view_snaps', |
765 | 591 | 'create_charm_recipe', | ||
766 | 591 | 'create_snap', | 592 | 'create_snap', |
767 | 592 | ] | 593 | ] |
768 | 593 | 594 | ||
769 | diff --git a/lib/lp/registry/templates/product-index.pt b/lib/lp/registry/templates/product-index.pt | |||
770 | index 242483d..fc3c274 100644 | |||
771 | --- a/lib/lp/registry/templates/product-index.pt | |||
772 | +++ b/lib/lp/registry/templates/product-index.pt | |||
773 | @@ -170,25 +170,35 @@ | |||
774 | 170 | </p> | 170 | </p> |
775 | 171 | 171 | ||
776 | 172 | <ul class="horizontal" id="project-link-info"> | 172 | <ul class="horizontal" id="project-link-info"> |
778 | 173 | <li tal:condition="overview_menu/series_add/enabled"> | 173 | <li class="nowrap" |
779 | 174 | tal:condition="overview_menu/series_add/enabled"> | ||
780 | 174 | <a tal:replace="structure overview_menu/series_add/fmt:link" /> | 175 | <a tal:replace="structure overview_menu/series_add/fmt:link" /> |
781 | 175 | </li> | 176 | </li> |
783 | 176 | <li> | 177 | <li class="nowrap"> |
784 | 177 | <a tal:replace="structure overview_menu/milestones/fmt:link" /> | 178 | <a tal:replace="structure overview_menu/milestones/fmt:link" /> |
785 | 178 | </li> | 179 | </li> |
787 | 179 | <li tal:define="link context/menu:overview/view_recipes" | 180 | <li class="nowrap" |
788 | 181 | tal:define="link context/menu:overview/view_recipes" | ||
789 | 180 | tal:condition="link/enabled"> | 182 | tal:condition="link/enabled"> |
790 | 181 | <a tal:replace="structure link/fmt:link" /> | 183 | <a tal:replace="structure link/fmt:link" /> |
791 | 182 | </li> | 184 | </li> |
793 | 183 | <li tal:define="link context/menu:overview/view_snaps" | 185 | <li class="nowrap" |
794 | 186 | tal:define="link context/menu:overview/view_snaps" | ||
795 | 184 | tal:condition="link/enabled"> | 187 | tal:condition="link/enabled"> |
796 | 185 | <a tal:replace="structure link/fmt:link" /> | 188 | <a tal:replace="structure link/fmt:link" /> |
797 | 186 | </li> | 189 | </li> |
799 | 187 | <li tal:define="link context/menu:overview/create_snap" | 190 | <li class="nowrap" |
800 | 191 | tal:define="link context/menu:overview/create_snap" | ||
801 | 188 | tal:condition="link/enabled"> | 192 | tal:condition="link/enabled"> |
802 | 189 | <a tal:replace="structure link/fmt:link" /> | 193 | <a tal:replace="structure link/fmt:link" /> |
803 | 190 | </li> | 194 | </li> |
805 | 191 | <li tal:define="link context/menu:overview/view_charm_recipes" | 195 | <li class="nowrap" |
806 | 196 | tal:define="link context/menu:overview/view_charm_recipes" | ||
807 | 197 | tal:condition="link/enabled"> | ||
808 | 198 | <a tal:replace="structure link/fmt:link" /> | ||
809 | 199 | </li> | ||
810 | 200 | <li class="nowrap" | ||
811 | 201 | tal:define="link context/menu:overview/create_charm_recipe" | ||
812 | 192 | tal:condition="link/enabled"> | 202 | tal:condition="link/enabled"> |
813 | 193 | <a tal:replace="structure link/fmt:link" /> | 203 | <a tal:replace="structure link/fmt:link" /> |
814 | 194 | </li> | 204 | </li> |