Merge lp:~wallyworld/launchpad/inline-recipe-distro-series-edit into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 12646
Proposed branch: lp:~wallyworld/launchpad/inline-recipe-distro-series-edit
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/inline-multicheckbox-widget
Diff against target: 625 lines (+207/-76)
11 files modified
lib/lp/app/doc/lazr-js-widgets.txt (+5/-8)
lib/lp/code/browser/sourcepackagerecipe.py (+70/-23)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+8/-8)
lib/lp/code/configure.zcml (+4/-0)
lib/lp/code/interfaces/sourcepackagerecipe.py (+37/-24)
lib/lp/code/javascript/requestbuild_overlay.js (+2/-2)
lib/lp/code/model/sourcepackagerecipe.py (+6/-0)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+2/-9)
lib/lp/code/templates/sourcepackagerecipe-new.pt (+1/-1)
lib/lp/code/templates/sourcepackagerecipe-request-builds.pt (+1/-1)
lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py (+71/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/inline-recipe-distro-series-edit
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+52940@code.launchpad.net

Commit message

[r=abentley][bug=735899] Use the new inline multicheckbox selection widget to edit the source package recipe distroseries attribute.

Description of the change

Use the new inline multicheckbox selection widget to edit the source package recipe distroseries attribute.

== Implementation ==

Not much to tell - just wire up the new widget. Also incorporate Tim's recent changes to the lazr widget infrastructure.
To make everything glue together, the "distros" attribute of the SourcePackageRecipeEditSchema view interface was renamed to "distroseries" so that it becomes the same name as the underlying model attribute on the SourcePackageRecipe interface. The ISourcePackageRecipeEdit and ISourcePackageRecipeEditableAttributes interfaces also had to be reordered so referencing could work.

== Demo and QA ==

A screenshot of the widget in action:
http://people.canonical.com/~ianb/distroseries-popup.png
== Tests ==

New windmill test added.
/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py
bin/test -vvt test_inline_distroseries_edit

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/doc/lazr-js-widgets.txt
  lib/lp/code/configure.zcml
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/code/javascript/requestbuild_overlay.js
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/templates/sourcepackagerecipe-index.pt
  lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py

./lib/lp/code/javascript/requestbuild_overlay.js
     110: Line exceeds 78 characters.
     148: Line exceeds 78 characters.
     180: Line exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

This looks pretty good.

I wonder whether label_tag="dt" and items_tag="dd" could be defaults.

Sorry for using distros rather than distroseries. I was new to the problem domain.

I don't think you should have a setUp in RecipeEdit until have more than one test, because until you have more tests, you won't know what needs to vary.

I also see you setting up far more constants than you actually use, e.g. recipe name. Why not just use makeSourcePackageRecipe? (you could use "with person_logged_in(recipe.owner)".

The implementation of updateSeries is a bit sad. Is there no straightforward way to update that?

review: Needs Information
Revision history for this message
Ian Booth (wallyworld) wrote :

> This looks pretty good.
>
> I wonder whether label_tag="dt" and items_tag="dd" could be defaults.
>

Yeah, thought about that but with a sample size of 1 - this is the only place it's used so far - wasn't sure about whether to encode the assumption the widget would be rendered in a definition list or not. In the end, I thought it safer to go with span. We can always revisit when we get more places using the widget.

> Sorry for using distros rather than distroseries. I was new to the problem
> domain.
>

No probs.

> I don't think you should have a setUp in RecipeEdit until have more than one
> test, because until you have more tests, you won't know what needs to vary.
>

Ok. Will fix.

> I also see you setting up far more constants than you actually use, e.g.
> recipe name. Why not just use makeSourcePackageRecipe? (you could use "with
> person_logged_in(recipe.owner)".
>

Will take alook.

> The implementation of updateSeries is a bit sad. Is there no straightforward
> way to update that?

Ah, the way that is there is absolutely required if the distroseries field is exported as a CollectionField, which it was initially. However, now that the distroseries field is a List, that approach may not be required anymore. I'll have a look.

Revision history for this message
Aaron Bentley (abentley) :
review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :

>
> > The implementation of updateSeries is a bit sad. Is there no
> straightforward
> > way to update that?
>
> Ah, the way that is there is absolutely required if the distroseries field is
> exported as a CollectionField, which it was initially. However, now that the
> distroseries field is a List, that approach may not be required anymore. I'll
> have a look.

The current implementation is still required even when distroseries is exported as a List. The property has a type which subclasses ResultSet and so one needs to use clear(), add(), remove() methods.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/doc/lazr-js-widgets.txt'
2--- lib/lp/app/doc/lazr-js-widgets.txt 2011-03-22 02:47:21 +0000
3+++ lib/lp/app/doc/lazr-js-widgets.txt 2011-03-22 02:47:23 +0000
4@@ -219,7 +219,8 @@
5 descriptions. since the barrier to create a PPA is relatively low, we
6 restrict the linkability of some fields. The constructor provides a
7 "linkify_text" parameter that defaults to True. Set this to False to avoid
8-the linkification of text. See the IArchive['description'] editor for an example.
9+the linkification of text. See the IArchive['description'] editor for an
10+example.
11
12
13 InlineEditPickerWidget
14@@ -350,9 +351,9 @@
15
16 >>> from lp.app.browser.lazrjs import EnumChoiceWidget
17
18-As with the other widgets, this one requires a context object and a Choice type
19-field. The rendering of the widget hooks up to the lazr ChoiceSource with the
20-standard patch plugin.
21+As with the other widgets, this one requires a context object and a Choice
22+type field. The rendering of the widget hooks up to the lazr ChoiceSource
23+with the standard patch plugin.
24
25 One of the different things about this widget is the styles that are added.
26 Many enums have specific colour styles. Generally these are the names of
27@@ -459,22 +460,18 @@
28 >>> login_person(eric)
29 >>> print widget()
30 <span id="edit-distroseries">
31- <BLANKLINE>
32 <dt>
33 Recipe distro series
34- <BLANKLINE>
35 <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
36 id="edit-distroseries-btn">
37 Edit
38 </button>
39- <BLANKLINE>
40 <noscript>
41 <a class="sprite edit"
42 href="http://code.launchpad.dev/~eric/+recipe/cake_recipe/+edit"
43 title=""></a>
44 </noscript>
45 </dt>
46- <BLANKLINE>
47 <span class="yui3-activator-data-box">
48 <dl id='edit-distroseries-items'>
49 ...
50
51=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
52--- lib/lp/code/browser/sourcepackagerecipe.py 2011-03-18 10:31:56 +0000
53+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
54@@ -24,15 +24,21 @@
55 from lazr.lifecycle.event import ObjectModifiedEvent
56 from lazr.lifecycle.snapshot import Snapshot
57 from lazr.restful.interface import use_template
58+from lazr.restful.interfaces import (
59+ IFieldHTMLRenderer,
60+ IWebServiceClientRequest,
61+ )
62 import simplejson
63 from storm.locals import Store
64 from z3c.ptcompat import ViewPageTemplateFile
65+from zope import component
66 from zope.app.form.browser.widget import Widget
67 from zope.app.form.interfaces import IView
68 from zope.component import getUtility
69 from zope.event import notify
70 from zope.formlib import form
71 from zope.interface import (
72+ implementer,
73 implements,
74 Interface,
75 providedBy,
76@@ -44,6 +50,7 @@
77 Text,
78 TextLine,
79 )
80+from zope.schema.interfaces import ICollection
81 from zope.schema.vocabulary import (
82 SimpleTerm,
83 SimpleVocabulary,
84@@ -296,6 +303,43 @@
85 title = "Edit the recipe name"
86 return TextLineEditorWidget(self.context, name, title, 'h1')
87
88+ @property
89+ def distroseries_widget(self):
90+ from lp.app.browser.lazrjs import InlineMultiCheckboxWidget
91+ field = ISourcePackageEditSchema['distroseries']
92+ return InlineMultiCheckboxWidget(
93+ self.context,
94+ field,
95+ attribute_type="reference",
96+ vocabulary='BuildableDistroSeries',
97+ label="Distribution series:",
98+ label_tag="dt",
99+ header="Change default distribution series:",
100+ empty_display_value="None",
101+ selected_items=sorted(
102+ self.context.distroseries, key=lambda ds: ds.displayname),
103+ items_tag="dd",
104+ )
105+
106+
107+@component.adapter(ISourcePackageRecipe, ICollection,
108+ IWebServiceClientRequest)
109+@implementer(IFieldHTMLRenderer)
110+def distroseries_renderer(context, field, request):
111+ """Render a distroseries collection as a set of links."""
112+
113+ def render(value):
114+ distroseries = sorted(
115+ context.distroseries, key=lambda ds: ds.displayname)
116+ if not distroseries:
117+ return 'None'
118+ html = "<ul>"
119+ html += ''.join(
120+ ["<li>%s</li>" % format_link(series) for series in distroseries])
121+ html += "</ul>"
122+ return html
123+ return render
124+
125
126 def builds_for_recipe(recipe):
127 """A list of interesting builds.
128@@ -337,7 +381,7 @@
129
130 The distroseries function as defaults for requesting a build.
131 """
132- initial_values = {'distros': self.context.distroseries}
133+ initial_values = {'distroseries': self.context.distroseries}
134 build = self.context.last_build
135 if build is not None:
136 initial_values['archive'] = build.archive
137@@ -346,26 +390,26 @@
138 class schema(Interface):
139 """Schema for requesting a build."""
140 archive = Choice(vocabulary='TargetPPAs', title=u'Archive')
141- distros = List(
142+ distroseries = List(
143 Choice(vocabulary='BuildableDistroSeries'),
144 title=u'Distribution series')
145
146- custom_widget('distros', LabeledMultiCheckBoxWidget)
147+ custom_widget('distroseries', LabeledMultiCheckBoxWidget)
148
149 def validate(self, data):
150- distros = data.get('distros', [])
151+ distros = data.get('distroseries', [])
152 if not len(distros):
153- self.setFieldError('distros',
154+ self.setFieldError('distroseries',
155 "You need to specify at least one distro series for which "
156 "to build.")
157 return
158 over_quota_distroseries = []
159- for distroseries in data['distros']:
160+ for distroseries in data['distroseries']:
161 if self.context.isOverQuota(self.user, distroseries):
162 over_quota_distroseries.append(str(distroseries))
163 if len(over_quota_distroseries) > 0:
164 self.setFieldError(
165- 'distros',
166+ 'distroseries',
167 "You have exceeded today's quota for %s." %
168 ', '.join(over_quota_distroseries))
169
170@@ -378,7 +422,7 @@
171 """
172 informational = {}
173 builds = []
174- for distroseries in data['distros']:
175+ for distroseries in data['distroseries']:
176 try:
177 build = self.context.requestBuild(
178 data['archive'], self.user, distroseries, manual=True)
179@@ -512,18 +556,13 @@
180 'description',
181 'owner',
182 'build_daily',
183+ 'distroseries',
184 ])
185 daily_build_archive = Choice(vocabulary='TargetPPAs',
186 title=u'Daily build archive',
187 description=(
188 u'If built daily, this is the archive where the package '
189 u'will be uploaded.'))
190- distros = List(
191- Choice(vocabulary='BuildableDistroSeries'),
192- title=u'Default distribution series',
193- description=(
194- u'If built daily, these are the distribution versions that '
195- u'the recipe will be built for.'))
196 recipe_text = has_structured_doc(
197 Text(
198 title=u'Recipe text', required=True,
199@@ -577,9 +616,9 @@
200
201 def validate(self, data):
202 if data['build_daily']:
203- if len(data['distros']) == 0:
204+ if len(data['distroseries']) == 0:
205 self.setFieldError(
206- 'distros',
207+ 'distroseries',
208 'You must specify at least one series for daily builds.')
209 try:
210 parser = RecipeParser(data['recipe_text'])
211@@ -673,7 +712,7 @@
212 title = label = 'Create a new source package recipe'
213
214 schema = ISourcePackageAddSchema
215- custom_widget('distros', LabeledMultiCheckBoxWidget)
216+ custom_widget('distroseries', LabeledMultiCheckBoxWidget)
217 custom_widget('owner', RecipeOwnerWidget)
218 custom_widget('use_ppa', LaunchpadRadioWidget)
219
220@@ -696,6 +735,11 @@
221 # all confused when we want to create a new PPA.
222 archive_widget._displayItemForMissingValue = False
223
224+ def setUpFields(self):
225+ super(SourcePackageRecipeAddView, self).setUpFields()
226+ # Ensure distro series widget allows input
227+ self.form_fields['distroseries'].for_input = True
228+
229 def getBranch(self):
230 """The branch on which the recipe is built."""
231 return self.context
232@@ -729,7 +773,7 @@
233 'name': self._find_unused_name(self.user),
234 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,
235 'owner': self.user,
236- 'distros': series,
237+ 'distroseries': series,
238 'build_daily': True,
239 'use_ppa': EXISTING_PPA,
240 }
241@@ -750,8 +794,8 @@
242 source_package_recipe = self.error_handler(
243 getUtility(ISourcePackageRecipeSource).new,
244 self.user, owner, data['name'],
245- data['recipe_text'], data['description'], data['distros'],
246- ppa, data['build_daily'])
247+ data['recipe_text'], data['description'],
248+ data['distroseries'], ppa, data['build_daily'])
249 Store.of(source_package_recipe).flush()
250 except ErrorHandled:
251 return
252@@ -795,11 +839,14 @@
253 label = title
254
255 schema = ISourcePackageEditSchema
256- custom_widget('distros', LabeledMultiCheckBoxWidget)
257+ custom_widget('distroseries', LabeledMultiCheckBoxWidget)
258
259 def setUpFields(self):
260 super(SourcePackageRecipeEditView, self).setUpFields()
261
262+ # Ensure distro series widget allows input
263+ self.form_fields['distroseries'].for_input = True
264+
265 if check_permission('launchpad.Admin', self.context):
266 # Exclude the PPA archive dropdown.
267 self.form_fields = self.form_fields.omit('daily_build_archive')
268@@ -819,7 +866,7 @@
269 @property
270 def initial_values(self):
271 return {
272- 'distros': self.context.distroseries,
273+ 'distroseries': self.context.distroseries,
274 'recipe_text': self.context.recipe_text,
275 }
276
277@@ -843,7 +890,7 @@
278 except ErrorHandled:
279 return
280
281- distros = data.pop('distros')
282+ distros = data.pop('distroseries')
283 if distros != self.context.distroseries:
284 self.context.distroseries.clear()
285 for distroseries_item in distros:
286
287=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
288--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-03-18 10:31:56 +0000
289+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
290@@ -272,7 +272,7 @@
291 branch = self.factory.makeAnyBranch()
292 with person_logged_in(archive.owner):
293 view = create_initialized_view(branch, '+new-recipe')
294- series = set(view.initial_values['distros'])
295+ series = set(view.initial_values['distroseries'])
296 initial_series = set([development, current])
297 self.assertEqual(initial_series, series.intersection(initial_series))
298 other_series = set(
299@@ -452,7 +452,7 @@
300 browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')
301 browser.getControl(name='field.name').value = 'daily'
302 browser.getControl('Description').value = 'Make some food!'
303- browser.getControl(name='field.distros').value = []
304+ browser.getControl(name='field.distroseries').value = []
305 browser.getControl('Create Recipe').click()
306 self.assertEqual(
307 'You must specify at least one series for daily builds.',
308@@ -780,8 +780,8 @@
309 'lp://dev/~chef/ratatouille/meat',
310 MatchesTagText(content, 'edit-recipe_text'))
311 self.assertThat(
312- 'Distribution series: Mumbly Midget',
313- MatchesTagText(content, 'distros'))
314+ 'Distribution series: Edit Mumbly Midget',
315+ MatchesTagText(content, 'distroseries'))
316 self.assertThat(
317 'PPA 2', MatchesPickerText(content, 'edit-daily_build_archive'))
318
319@@ -797,7 +797,7 @@
320 view.request_action.success({
321 'name': u'fings',
322 'recipe_text': recipe.recipe_text,
323- 'distros': recipe.distroseries})
324+ 'distroseries': recipe.distroseries})
325 self.assertSqlAttributeEqualsDate(
326 recipe, 'date_last_modified', UTC_NOW)
327
328@@ -846,8 +846,8 @@
329 'lp://dev/~chef/ratatouille/meat',
330 MatchesTagText(content, 'edit-recipe_text'))
331 self.assertThat(
332- 'Distribution series: Mumbly Midget',
333- MatchesTagText(content, 'distros'))
334+ 'Distribution series: Edit Mumbly Midget',
335+ MatchesTagText(content, 'distroseries'))
336
337 def test_edit_recipe_forbidden_instruction(self):
338 self.factory.makeDistroSeries(
339@@ -1082,7 +1082,7 @@
340 Base branch: lp://dev/~chef/chocolate/cake
341 Debian version: {debupstream}-0~{revno}
342 Daily build archive: Secret PPA Edit
343- Distribution series: Secret Squirrel
344+ Distribution series: Edit Secret Squirrel
345
346 Latest builds
347 Status When complete Distribution series Archive
348
349=== modified file 'lib/lp/code/configure.zcml'
350--- lib/lp/code/configure.zcml 2011-03-16 06:03:44 +0000
351+++ lib/lp/code/configure.zcml 2011-03-22 02:47:23 +0000
352@@ -1001,6 +1001,10 @@
353 name="RECIPEBRANCHBUILD"
354 permission="zope.Public"/>
355
356+ <adapter
357+ factory="lp.code.browser.sourcepackagerecipe.distroseries_renderer"
358+ name="distroseries"/>
359+
360 <!-- RecipeBuildRecordSet and related classes-->
361
362 <securedutility
363
364=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
365--- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-03-16 06:03:44 +0000
366+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
367@@ -36,6 +36,7 @@
368 from lazr.restful.fields import (
369 CollectionField,
370 Reference,
371+ ReferenceChoice,
372 )
373 from lazr.restful.interface import copy_field
374 from zope.interface import (
375@@ -47,6 +48,7 @@
376 Choice,
377 Datetime,
378 Int,
379+ List,
380 Text,
381 TextLine,
382 )
383@@ -181,26 +183,6 @@
384 """
385
386
387-class ISourcePackageRecipeEdit(Interface):
388- """ISourcePackageRecipe methods that require launchpad.Edit permission."""
389-
390- @mutator_for(ISourcePackageRecipeView['recipe_text'])
391- @operation_for_version("devel")
392- @operation_parameters(
393- recipe_text=copy_field(
394- ISourcePackageRecipeView['recipe_text']))
395- @export_write_operation()
396- def setRecipeText(recipe_text):
397- """Set the text of the recipe."""
398-
399- def destroySelf():
400- """Remove this SourcePackageRecipe from the database.
401-
402- This requires deleting any rows with non-nullable foreign key
403- references to this object.
404- """
405-
406-
407 class ISourcePackageRecipeEditableAttributes(IHasOwner):
408 """ISourcePackageRecipe attributes that can be edited.
409
410@@ -219,10 +201,13 @@
411 vocabulary='UserTeamsParticipationPlusSelf',
412 description=_("The person or team who can edit this recipe.")))
413
414- distroseries = CollectionField(
415- Reference(IDistroSeries), title=_("The distroseries this recipe will"
416- " build a source package for"),
417- readonly=False)
418+ distroseries = exported(List(
419+ ReferenceChoice(schema=IDistroSeries,
420+ vocabulary='BuildableDistroSeries'),
421+ title=_("Default distribution series"),
422+ description=_("If built daily, these are the distribution "
423+ "versions that the recipe will be built for."),
424+ readonly=True))
425 build_daily = exported(Bool(
426 title=_("Built daily"),
427 description=_(
428@@ -245,6 +230,34 @@
429 is_stale = Bool(title=_('Recipe is stale.'))
430
431
432+class ISourcePackageRecipeEdit(Interface):
433+ """ISourcePackageRecipe methods that require launchpad.Edit permission."""
434+
435+ @mutator_for(ISourcePackageRecipeView['recipe_text'])
436+ @operation_for_version("devel")
437+ @operation_parameters(
438+ recipe_text=copy_field(
439+ ISourcePackageRecipeView['recipe_text']))
440+ @export_write_operation()
441+ def setRecipeText(recipe_text):
442+ """Set the text of the recipe."""
443+
444+ @mutator_for(ISourcePackageRecipeEditableAttributes['distroseries'])
445+ @operation_parameters(distroseries=copy_field(
446+ ISourcePackageRecipeEditableAttributes['distroseries']))
447+ @export_write_operation()
448+ @operation_for_version("devel")
449+ def updateSeries(distroseries):
450+ """Replace this recipe's distro series."""
451+
452+ def destroySelf():
453+ """Remove this SourcePackageRecipe from the database.
454+
455+ This requires deleting any rows with non-nullable foreign key
456+ references to this object.
457+ """
458+
459+
460 class ISourcePackageRecipe(ISourcePackageRecipeData,
461 ISourcePackageRecipeEdit, ISourcePackageRecipeEditableAttributes,
462 ISourcePackageRecipeView):
463
464=== modified file 'lib/lp/code/javascript/requestbuild_overlay.js'
465--- lib/lp/code/javascript/requestbuild_overlay.js 2011-03-11 23:54:32 +0000
466+++ lib/lp/code/javascript/requestbuild_overlay.js 2011-03-22 02:47:23 +0000
467@@ -250,7 +250,7 @@
468 * Return: true if data is valid
469 */
470 function validate(data) {
471- var distros = data['field.distros']
472+ var distros = data['field.distroseries']
473 if (Y.Object.size(distros) == 0) {
474 request_build_response_handler.showError(
475 "You need to specify at least one distro series for " +
476@@ -391,7 +391,7 @@
477
478 function get_distroseries_nodes() {
479 return request_build_overlay.form_node.all(
480- "label[for^='field.distros.']");
481+ "label[for^='field.distroseries.']");
482 }
483
484 var DISABLED_DISTROSERIES_CHECKBOX_HTML =
485
486=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
487--- lib/lp/code/model/sourcepackagerecipe.py 2011-03-18 06:47:15 +0000
488+++ lib/lp/code/model/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
489@@ -187,6 +187,12 @@
490 def recipe_text(self):
491 return self.builder_recipe.get_recipe_text()
492
493+ def updateSeries(self, distroseries):
494+ if distroseries != self.distroseries:
495+ self.distroseries.clear()
496+ for distroseries_item in distroseries:
497+ self.distroseries.add(distroseries_item)
498+
499 @staticmethod
500 def new(registrant, owner, name, recipe, description,
501 distroseries=None, daily_build_archive=None, build_daily=False,
502
503=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
504--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-03-02 01:47:19 +0000
505+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-03-22 02:47:23 +0000
506@@ -118,15 +118,8 @@
507 <dt>Daily build archive:</dt>
508 <dd tal:content="structure view/archive_picker"/>
509 </dl>
510-
511- <dl id="distros">
512- <dt>Distribution series:</dt>
513- <dd>
514- <ul>
515- <li tal:repeat="curseries context/distroseries"
516- tal:content="structure curseries/fmt:link" />
517- </ul>
518- </dd>
519+ <dl id="distroseries">
520+ <tal:distroseries tal:replace="structure view/distroseries_widget"/>
521 </dl>
522 </div>
523 </div>
524
525=== modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
526--- lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-02-14 01:48:57 +0000
527+++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-03-22 02:47:23 +0000
528@@ -99,7 +99,7 @@
529 </tal:widget>
530 </tal:create-ppa>
531
532- <tal:widget define="widget nocall:view/widgets/distros">
533+ <tal:widget define="widget nocall:view/widgets/distroseries">
534 <metal:block use-macro="context/@@launchpad_form/widget_row" />
535 </tal:widget>
536 <tal:widget define="widget nocall:view/widgets/recipe_text">
537
538=== modified file 'lib/lp/code/templates/sourcepackagerecipe-request-builds.pt'
539--- lib/lp/code/templates/sourcepackagerecipe-request-builds.pt 2010-03-31 19:27:07 +0000
540+++ lib/lp/code/templates/sourcepackagerecipe-request-builds.pt 2011-03-22 02:47:23 +0000
541@@ -16,7 +16,7 @@
542 <tal:widget define="widget nocall:view/widgets/archive">
543 <metal:block use-macro="context/@@launchpad_form/widget_row" />
544 </tal:widget>
545- <tal:widget define="widget nocall:view/widgets/distros">
546+ <tal:widget define="widget nocall:view/widgets/distroseries">
547 <metal:block use-macro="context/@@launchpad_form/widget_row" />
548 </tal:widget>
549 </table>
550
551=== added file 'lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py'
552--- lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py 1970-01-01 00:00:00 +0000
553+++ lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py 2011-03-22 02:47:23 +0000
554@@ -0,0 +1,71 @@
555+# Copyright 2011 Canonical Ltd. This software is licensed under the
556+# GNU Affero General Public License version 3 (see the file LICENSE).
557+
558+"""Tests for requesting recipe builds."""
559+
560+__metaclass__ = type
561+__all__ = []
562+
563+import transaction
564+
565+from zope.component import getUtility
566+from storm.store import Store
567+
568+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
569+from lp.testing import WindmillTestCase
570+from lp.testing.windmill.constants import (
571+ FOR_ELEMENT,
572+ PAGE_LOAD,
573+ )
574+from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
575+from lp.code.windmill.testing import CodeWindmillLayer
576+
577+
578+class TestRecipeEdit(WindmillTestCase):
579+ """Test recipe editing with inline widgets."""
580+
581+ layer = CodeWindmillLayer
582+ suite_name = "Request recipe build"
583+
584+ def test_inline_distroseries_edit(self):
585+ """Test that inline editing of distroseries works."""
586+
587+ chef = self.factory.makePerson(
588+ displayname='Master Chef', name='chef', password='test',
589+ email="chef@example.com")
590+ recipe = self.factory.makeSourcePackageRecipe(owner=chef)
591+ transaction.commit()
592+
593+ client, start_url = self.getClientFor(recipe, user=chef)
594+ client.waits.forElement(
595+ id=u'edit-distroseries-items', timeout=PAGE_LOAD)
596+
597+ # Edit the distro series.
598+ client.click(jquery=u'("#edit-distroseries-btn")[0]')
599+ client.waits.forElement(
600+ jquery=u'("#edit-distroseries-save")',
601+ timeout=FOR_ELEMENT)
602+ # Click the checkbox to select the first distro series
603+ client.click(name=u'field.distroseries.0')
604+ client.waits.forElement(
605+ jquery=u"('[name=\"field.distroseries.0\"][checked=\"checked\"]')",
606+ timeout=FOR_ELEMENT)
607+ # Save it
608+ client.click(jquery=u'("#edit-distroseries-save")[0]')
609+
610+ # Wait for the the new one that is added.
611+ client.waits.forElement(
612+ jquery=u"('#edit-distroseries-items ul li a')[0]",
613+ timeout=FOR_ELEMENT)
614+
615+ # Check that the new data was saved.
616+ transaction.commit()
617+ hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
618+ store = Store.of(recipe)
619+ saved_recipe = store.find(
620+ SourcePackageRecipe,
621+ SourcePackageRecipe.name==recipe.name).one()
622+ self.assertEqual(len(list(saved_recipe.distroseries)), 2)
623+ distroseries=sorted(
624+ saved_recipe.distroseries, key=lambda ds: ds.displayname)
625+ self.assertEqual(distroseries[0], hoary)