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
=== modified file 'lib/lp/app/doc/lazr-js-widgets.txt'
--- lib/lp/app/doc/lazr-js-widgets.txt 2011-03-22 02:47:21 +0000
+++ lib/lp/app/doc/lazr-js-widgets.txt 2011-03-22 02:47:23 +0000
@@ -219,7 +219,8 @@
219descriptions. since the barrier to create a PPA is relatively low, we219descriptions. since the barrier to create a PPA is relatively low, we
220restrict the linkability of some fields. The constructor provides a220restrict the linkability of some fields. The constructor provides a
221"linkify_text" parameter that defaults to True. Set this to False to avoid221"linkify_text" parameter that defaults to True. Set this to False to avoid
222the linkification of text. See the IArchive['description'] editor for an example.222the linkification of text. See the IArchive['description'] editor for an
223example.
223224
224225
225InlineEditPickerWidget226InlineEditPickerWidget
@@ -350,9 +351,9 @@
350351
351 >>> from lp.app.browser.lazrjs import EnumChoiceWidget352 >>> from lp.app.browser.lazrjs import EnumChoiceWidget
352353
353As with the other widgets, this one requires a context object and a Choice type354As with the other widgets, this one requires a context object and a Choice
354field. The rendering of the widget hooks up to the lazr ChoiceSource with the355type field. The rendering of the widget hooks up to the lazr ChoiceSource
355standard patch plugin.356with the standard patch plugin.
356357
357One of the different things about this widget is the styles that are added.358One of the different things about this widget is the styles that are added.
358Many enums have specific colour styles. Generally these are the names of359Many enums have specific colour styles. Generally these are the names of
@@ -459,22 +460,18 @@
459 >>> login_person(eric)460 >>> login_person(eric)
460 >>> print widget()461 >>> print widget()
461 <span id="edit-distroseries">462 <span id="edit-distroseries">
462 <BLANKLINE>
463 <dt>463 <dt>
464 Recipe distro series464 Recipe distro series
465 <BLANKLINE>
466 <button class="lazr-btn yui3-activator-act yui3-activator-hidden"465 <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
467 id="edit-distroseries-btn">466 id="edit-distroseries-btn">
468 Edit467 Edit
469 </button>468 </button>
470 <BLANKLINE>
471 <noscript>469 <noscript>
472 <a class="sprite edit"470 <a class="sprite edit"
473 href="http://code.launchpad.dev/~eric/+recipe/cake_recipe/+edit"471 href="http://code.launchpad.dev/~eric/+recipe/cake_recipe/+edit"
474 title=""></a>472 title=""></a>
475 </noscript>473 </noscript>
476 </dt>474 </dt>
477 <BLANKLINE>
478 <span class="yui3-activator-data-box">475 <span class="yui3-activator-data-box">
479 <dl id='edit-distroseries-items'>476 <dl id='edit-distroseries-items'>
480 ...477 ...
481478
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-03-18 10:31:56 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
@@ -24,15 +24,21 @@
24from lazr.lifecycle.event import ObjectModifiedEvent24from lazr.lifecycle.event import ObjectModifiedEvent
25from lazr.lifecycle.snapshot import Snapshot25from lazr.lifecycle.snapshot import Snapshot
26from lazr.restful.interface import use_template26from lazr.restful.interface import use_template
27from lazr.restful.interfaces import (
28 IFieldHTMLRenderer,
29 IWebServiceClientRequest,
30 )
27import simplejson31import simplejson
28from storm.locals import Store32from storm.locals import Store
29from z3c.ptcompat import ViewPageTemplateFile33from z3c.ptcompat import ViewPageTemplateFile
34from zope import component
30from zope.app.form.browser.widget import Widget35from zope.app.form.browser.widget import Widget
31from zope.app.form.interfaces import IView36from zope.app.form.interfaces import IView
32from zope.component import getUtility37from zope.component import getUtility
33from zope.event import notify38from zope.event import notify
34from zope.formlib import form39from zope.formlib import form
35from zope.interface import (40from zope.interface import (
41 implementer,
36 implements,42 implements,
37 Interface,43 Interface,
38 providedBy,44 providedBy,
@@ -44,6 +50,7 @@
44 Text,50 Text,
45 TextLine,51 TextLine,
46 )52 )
53from zope.schema.interfaces import ICollection
47from zope.schema.vocabulary import (54from zope.schema.vocabulary import (
48 SimpleTerm,55 SimpleTerm,
49 SimpleVocabulary,56 SimpleVocabulary,
@@ -296,6 +303,43 @@
296 title = "Edit the recipe name"303 title = "Edit the recipe name"
297 return TextLineEditorWidget(self.context, name, title, 'h1')304 return TextLineEditorWidget(self.context, name, title, 'h1')
298305
306 @property
307 def distroseries_widget(self):
308 from lp.app.browser.lazrjs import InlineMultiCheckboxWidget
309 field = ISourcePackageEditSchema['distroseries']
310 return InlineMultiCheckboxWidget(
311 self.context,
312 field,
313 attribute_type="reference",
314 vocabulary='BuildableDistroSeries',
315 label="Distribution series:",
316 label_tag="dt",
317 header="Change default distribution series:",
318 empty_display_value="None",
319 selected_items=sorted(
320 self.context.distroseries, key=lambda ds: ds.displayname),
321 items_tag="dd",
322 )
323
324
325@component.adapter(ISourcePackageRecipe, ICollection,
326 IWebServiceClientRequest)
327@implementer(IFieldHTMLRenderer)
328def distroseries_renderer(context, field, request):
329 """Render a distroseries collection as a set of links."""
330
331 def render(value):
332 distroseries = sorted(
333 context.distroseries, key=lambda ds: ds.displayname)
334 if not distroseries:
335 return 'None'
336 html = "<ul>"
337 html += ''.join(
338 ["<li>%s</li>" % format_link(series) for series in distroseries])
339 html += "</ul>"
340 return html
341 return render
342
299343
300def builds_for_recipe(recipe):344def builds_for_recipe(recipe):
301 """A list of interesting builds.345 """A list of interesting builds.
@@ -337,7 +381,7 @@
337381
338 The distroseries function as defaults for requesting a build.382 The distroseries function as defaults for requesting a build.
339 """383 """
340 initial_values = {'distros': self.context.distroseries}384 initial_values = {'distroseries': self.context.distroseries}
341 build = self.context.last_build385 build = self.context.last_build
342 if build is not None:386 if build is not None:
343 initial_values['archive'] = build.archive387 initial_values['archive'] = build.archive
@@ -346,26 +390,26 @@
346 class schema(Interface):390 class schema(Interface):
347 """Schema for requesting a build."""391 """Schema for requesting a build."""
348 archive = Choice(vocabulary='TargetPPAs', title=u'Archive')392 archive = Choice(vocabulary='TargetPPAs', title=u'Archive')
349 distros = List(393 distroseries = List(
350 Choice(vocabulary='BuildableDistroSeries'),394 Choice(vocabulary='BuildableDistroSeries'),
351 title=u'Distribution series')395 title=u'Distribution series')
352396
353 custom_widget('distros', LabeledMultiCheckBoxWidget)397 custom_widget('distroseries', LabeledMultiCheckBoxWidget)
354398
355 def validate(self, data):399 def validate(self, data):
356 distros = data.get('distros', [])400 distros = data.get('distroseries', [])
357 if not len(distros):401 if not len(distros):
358 self.setFieldError('distros',402 self.setFieldError('distroseries',
359 "You need to specify at least one distro series for which "403 "You need to specify at least one distro series for which "
360 "to build.")404 "to build.")
361 return405 return
362 over_quota_distroseries = []406 over_quota_distroseries = []
363 for distroseries in data['distros']:407 for distroseries in data['distroseries']:
364 if self.context.isOverQuota(self.user, distroseries):408 if self.context.isOverQuota(self.user, distroseries):
365 over_quota_distroseries.append(str(distroseries))409 over_quota_distroseries.append(str(distroseries))
366 if len(over_quota_distroseries) > 0:410 if len(over_quota_distroseries) > 0:
367 self.setFieldError(411 self.setFieldError(
368 'distros',412 'distroseries',
369 "You have exceeded today's quota for %s." %413 "You have exceeded today's quota for %s." %
370 ', '.join(over_quota_distroseries))414 ', '.join(over_quota_distroseries))
371415
@@ -378,7 +422,7 @@
378 """422 """
379 informational = {}423 informational = {}
380 builds = []424 builds = []
381 for distroseries in data['distros']:425 for distroseries in data['distroseries']:
382 try:426 try:
383 build = self.context.requestBuild(427 build = self.context.requestBuild(
384 data['archive'], self.user, distroseries, manual=True)428 data['archive'], self.user, distroseries, manual=True)
@@ -512,18 +556,13 @@
512 'description',556 'description',
513 'owner',557 'owner',
514 'build_daily',558 'build_daily',
559 'distroseries',
515 ])560 ])
516 daily_build_archive = Choice(vocabulary='TargetPPAs',561 daily_build_archive = Choice(vocabulary='TargetPPAs',
517 title=u'Daily build archive',562 title=u'Daily build archive',
518 description=(563 description=(
519 u'If built daily, this is the archive where the package '564 u'If built daily, this is the archive where the package '
520 u'will be uploaded.'))565 u'will be uploaded.'))
521 distros = List(
522 Choice(vocabulary='BuildableDistroSeries'),
523 title=u'Default distribution series',
524 description=(
525 u'If built daily, these are the distribution versions that '
526 u'the recipe will be built for.'))
527 recipe_text = has_structured_doc(566 recipe_text = has_structured_doc(
528 Text(567 Text(
529 title=u'Recipe text', required=True,568 title=u'Recipe text', required=True,
@@ -577,9 +616,9 @@
577616
578 def validate(self, data):617 def validate(self, data):
579 if data['build_daily']:618 if data['build_daily']:
580 if len(data['distros']) == 0:619 if len(data['distroseries']) == 0:
581 self.setFieldError(620 self.setFieldError(
582 'distros',621 'distroseries',
583 'You must specify at least one series for daily builds.')622 'You must specify at least one series for daily builds.')
584 try:623 try:
585 parser = RecipeParser(data['recipe_text'])624 parser = RecipeParser(data['recipe_text'])
@@ -673,7 +712,7 @@
673 title = label = 'Create a new source package recipe'712 title = label = 'Create a new source package recipe'
674713
675 schema = ISourcePackageAddSchema714 schema = ISourcePackageAddSchema
676 custom_widget('distros', LabeledMultiCheckBoxWidget)715 custom_widget('distroseries', LabeledMultiCheckBoxWidget)
677 custom_widget('owner', RecipeOwnerWidget)716 custom_widget('owner', RecipeOwnerWidget)
678 custom_widget('use_ppa', LaunchpadRadioWidget)717 custom_widget('use_ppa', LaunchpadRadioWidget)
679718
@@ -696,6 +735,11 @@
696 # all confused when we want to create a new PPA.735 # all confused when we want to create a new PPA.
697 archive_widget._displayItemForMissingValue = False736 archive_widget._displayItemForMissingValue = False
698737
738 def setUpFields(self):
739 super(SourcePackageRecipeAddView, self).setUpFields()
740 # Ensure distro series widget allows input
741 self.form_fields['distroseries'].for_input = True
742
699 def getBranch(self):743 def getBranch(self):
700 """The branch on which the recipe is built."""744 """The branch on which the recipe is built."""
701 return self.context745 return self.context
@@ -729,7 +773,7 @@
729 'name': self._find_unused_name(self.user),773 'name': self._find_unused_name(self.user),
730 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,774 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,
731 'owner': self.user,775 'owner': self.user,
732 'distros': series,776 'distroseries': series,
733 'build_daily': True,777 'build_daily': True,
734 'use_ppa': EXISTING_PPA,778 'use_ppa': EXISTING_PPA,
735 }779 }
@@ -750,8 +794,8 @@
750 source_package_recipe = self.error_handler(794 source_package_recipe = self.error_handler(
751 getUtility(ISourcePackageRecipeSource).new,795 getUtility(ISourcePackageRecipeSource).new,
752 self.user, owner, data['name'],796 self.user, owner, data['name'],
753 data['recipe_text'], data['description'], data['distros'],797 data['recipe_text'], data['description'],
754 ppa, data['build_daily'])798 data['distroseries'], ppa, data['build_daily'])
755 Store.of(source_package_recipe).flush()799 Store.of(source_package_recipe).flush()
756 except ErrorHandled:800 except ErrorHandled:
757 return801 return
@@ -795,11 +839,14 @@
795 label = title839 label = title
796840
797 schema = ISourcePackageEditSchema841 schema = ISourcePackageEditSchema
798 custom_widget('distros', LabeledMultiCheckBoxWidget)842 custom_widget('distroseries', LabeledMultiCheckBoxWidget)
799843
800 def setUpFields(self):844 def setUpFields(self):
801 super(SourcePackageRecipeEditView, self).setUpFields()845 super(SourcePackageRecipeEditView, self).setUpFields()
802846
847 # Ensure distro series widget allows input
848 self.form_fields['distroseries'].for_input = True
849
803 if check_permission('launchpad.Admin', self.context):850 if check_permission('launchpad.Admin', self.context):
804 # Exclude the PPA archive dropdown.851 # Exclude the PPA archive dropdown.
805 self.form_fields = self.form_fields.omit('daily_build_archive')852 self.form_fields = self.form_fields.omit('daily_build_archive')
@@ -819,7 +866,7 @@
819 @property866 @property
820 def initial_values(self):867 def initial_values(self):
821 return {868 return {
822 'distros': self.context.distroseries,869 'distroseries': self.context.distroseries,
823 'recipe_text': self.context.recipe_text,870 'recipe_text': self.context.recipe_text,
824 }871 }
825872
@@ -843,7 +890,7 @@
843 except ErrorHandled:890 except ErrorHandled:
844 return891 return
845892
846 distros = data.pop('distros')893 distros = data.pop('distroseries')
847 if distros != self.context.distroseries:894 if distros != self.context.distroseries:
848 self.context.distroseries.clear()895 self.context.distroseries.clear()
849 for distroseries_item in distros:896 for distroseries_item in distros:
850897
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-03-18 10:31:56 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
@@ -272,7 +272,7 @@
272 branch = self.factory.makeAnyBranch()272 branch = self.factory.makeAnyBranch()
273 with person_logged_in(archive.owner):273 with person_logged_in(archive.owner):
274 view = create_initialized_view(branch, '+new-recipe')274 view = create_initialized_view(branch, '+new-recipe')
275 series = set(view.initial_values['distros'])275 series = set(view.initial_values['distroseries'])
276 initial_series = set([development, current])276 initial_series = set([development, current])
277 self.assertEqual(initial_series, series.intersection(initial_series))277 self.assertEqual(initial_series, series.intersection(initial_series))
278 other_series = set(278 other_series = set(
@@ -452,7 +452,7 @@
452 browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')452 browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')
453 browser.getControl(name='field.name').value = 'daily'453 browser.getControl(name='field.name').value = 'daily'
454 browser.getControl('Description').value = 'Make some food!'454 browser.getControl('Description').value = 'Make some food!'
455 browser.getControl(name='field.distros').value = []455 browser.getControl(name='field.distroseries').value = []
456 browser.getControl('Create Recipe').click()456 browser.getControl('Create Recipe').click()
457 self.assertEqual(457 self.assertEqual(
458 'You must specify at least one series for daily builds.',458 'You must specify at least one series for daily builds.',
@@ -780,8 +780,8 @@
780 'lp://dev/~chef/ratatouille/meat',780 'lp://dev/~chef/ratatouille/meat',
781 MatchesTagText(content, 'edit-recipe_text'))781 MatchesTagText(content, 'edit-recipe_text'))
782 self.assertThat(782 self.assertThat(
783 'Distribution series: Mumbly Midget',783 'Distribution series: Edit Mumbly Midget',
784 MatchesTagText(content, 'distros'))784 MatchesTagText(content, 'distroseries'))
785 self.assertThat(785 self.assertThat(
786 'PPA 2', MatchesPickerText(content, 'edit-daily_build_archive'))786 'PPA 2', MatchesPickerText(content, 'edit-daily_build_archive'))
787787
@@ -797,7 +797,7 @@
797 view.request_action.success({797 view.request_action.success({
798 'name': u'fings',798 'name': u'fings',
799 'recipe_text': recipe.recipe_text,799 'recipe_text': recipe.recipe_text,
800 'distros': recipe.distroseries})800 'distroseries': recipe.distroseries})
801 self.assertSqlAttributeEqualsDate(801 self.assertSqlAttributeEqualsDate(
802 recipe, 'date_last_modified', UTC_NOW)802 recipe, 'date_last_modified', UTC_NOW)
803803
@@ -846,8 +846,8 @@
846 'lp://dev/~chef/ratatouille/meat',846 'lp://dev/~chef/ratatouille/meat',
847 MatchesTagText(content, 'edit-recipe_text'))847 MatchesTagText(content, 'edit-recipe_text'))
848 self.assertThat(848 self.assertThat(
849 'Distribution series: Mumbly Midget',849 'Distribution series: Edit Mumbly Midget',
850 MatchesTagText(content, 'distros'))850 MatchesTagText(content, 'distroseries'))
851851
852 def test_edit_recipe_forbidden_instruction(self):852 def test_edit_recipe_forbidden_instruction(self):
853 self.factory.makeDistroSeries(853 self.factory.makeDistroSeries(
@@ -1082,7 +1082,7 @@
1082 Base branch: lp://dev/~chef/chocolate/cake1082 Base branch: lp://dev/~chef/chocolate/cake
1083 Debian version: {debupstream}-0~{revno}1083 Debian version: {debupstream}-0~{revno}
1084 Daily build archive: Secret PPA Edit1084 Daily build archive: Secret PPA Edit
1085 Distribution series: Secret Squirrel1085 Distribution series: Edit Secret Squirrel
10861086
1087 Latest builds1087 Latest builds
1088 Status When complete Distribution series Archive1088 Status When complete Distribution series Archive
10891089
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2011-03-16 06:03:44 +0000
+++ lib/lp/code/configure.zcml 2011-03-22 02:47:23 +0000
@@ -1001,6 +1001,10 @@
1001 name="RECIPEBRANCHBUILD"1001 name="RECIPEBRANCHBUILD"
1002 permission="zope.Public"/>1002 permission="zope.Public"/>
10031003
1004 <adapter
1005 factory="lp.code.browser.sourcepackagerecipe.distroseries_renderer"
1006 name="distroseries"/>
1007
1004 <!-- RecipeBuildRecordSet and related classes-->1008 <!-- RecipeBuildRecordSet and related classes-->
10051009
1006 <securedutility1010 <securedutility
10071011
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-03-16 06:03:44 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
@@ -36,6 +36,7 @@
36from lazr.restful.fields import (36from lazr.restful.fields import (
37 CollectionField,37 CollectionField,
38 Reference,38 Reference,
39 ReferenceChoice,
39 )40 )
40from lazr.restful.interface import copy_field41from lazr.restful.interface import copy_field
41from zope.interface import (42from zope.interface import (
@@ -47,6 +48,7 @@
47 Choice,48 Choice,
48 Datetime,49 Datetime,
49 Int,50 Int,
51 List,
50 Text,52 Text,
51 TextLine,53 TextLine,
52 )54 )
@@ -181,26 +183,6 @@
181 """183 """
182184
183185
184class ISourcePackageRecipeEdit(Interface):
185 """ISourcePackageRecipe methods that require launchpad.Edit permission."""
186
187 @mutator_for(ISourcePackageRecipeView['recipe_text'])
188 @operation_for_version("devel")
189 @operation_parameters(
190 recipe_text=copy_field(
191 ISourcePackageRecipeView['recipe_text']))
192 @export_write_operation()
193 def setRecipeText(recipe_text):
194 """Set the text of the recipe."""
195
196 def destroySelf():
197 """Remove this SourcePackageRecipe from the database.
198
199 This requires deleting any rows with non-nullable foreign key
200 references to this object.
201 """
202
203
204class ISourcePackageRecipeEditableAttributes(IHasOwner):186class ISourcePackageRecipeEditableAttributes(IHasOwner):
205 """ISourcePackageRecipe attributes that can be edited.187 """ISourcePackageRecipe attributes that can be edited.
206188
@@ -219,10 +201,13 @@
219 vocabulary='UserTeamsParticipationPlusSelf',201 vocabulary='UserTeamsParticipationPlusSelf',
220 description=_("The person or team who can edit this recipe.")))202 description=_("The person or team who can edit this recipe.")))
221203
222 distroseries = CollectionField(204 distroseries = exported(List(
223 Reference(IDistroSeries), title=_("The distroseries this recipe will"205 ReferenceChoice(schema=IDistroSeries,
224 " build a source package for"),206 vocabulary='BuildableDistroSeries'),
225 readonly=False)207 title=_("Default distribution series"),
208 description=_("If built daily, these are the distribution "
209 "versions that the recipe will be built for."),
210 readonly=True))
226 build_daily = exported(Bool(211 build_daily = exported(Bool(
227 title=_("Built daily"),212 title=_("Built daily"),
228 description=_(213 description=_(
@@ -245,6 +230,34 @@
245 is_stale = Bool(title=_('Recipe is stale.'))230 is_stale = Bool(title=_('Recipe is stale.'))
246231
247232
233class ISourcePackageRecipeEdit(Interface):
234 """ISourcePackageRecipe methods that require launchpad.Edit permission."""
235
236 @mutator_for(ISourcePackageRecipeView['recipe_text'])
237 @operation_for_version("devel")
238 @operation_parameters(
239 recipe_text=copy_field(
240 ISourcePackageRecipeView['recipe_text']))
241 @export_write_operation()
242 def setRecipeText(recipe_text):
243 """Set the text of the recipe."""
244
245 @mutator_for(ISourcePackageRecipeEditableAttributes['distroseries'])
246 @operation_parameters(distroseries=copy_field(
247 ISourcePackageRecipeEditableAttributes['distroseries']))
248 @export_write_operation()
249 @operation_for_version("devel")
250 def updateSeries(distroseries):
251 """Replace this recipe's distro series."""
252
253 def destroySelf():
254 """Remove this SourcePackageRecipe from the database.
255
256 This requires deleting any rows with non-nullable foreign key
257 references to this object.
258 """
259
260
248class ISourcePackageRecipe(ISourcePackageRecipeData,261class ISourcePackageRecipe(ISourcePackageRecipeData,
249 ISourcePackageRecipeEdit, ISourcePackageRecipeEditableAttributes,262 ISourcePackageRecipeEdit, ISourcePackageRecipeEditableAttributes,
250 ISourcePackageRecipeView):263 ISourcePackageRecipeView):
251264
=== modified file 'lib/lp/code/javascript/requestbuild_overlay.js'
--- lib/lp/code/javascript/requestbuild_overlay.js 2011-03-11 23:54:32 +0000
+++ lib/lp/code/javascript/requestbuild_overlay.js 2011-03-22 02:47:23 +0000
@@ -250,7 +250,7 @@
250 * Return: true if data is valid250 * Return: true if data is valid
251 */251 */
252function validate(data) {252function validate(data) {
253 var distros = data['field.distros']253 var distros = data['field.distroseries']
254 if (Y.Object.size(distros) == 0) {254 if (Y.Object.size(distros) == 0) {
255 request_build_response_handler.showError(255 request_build_response_handler.showError(
256 "You need to specify at least one distro series for " +256 "You need to specify at least one distro series for " +
@@ -391,7 +391,7 @@
391391
392function get_distroseries_nodes() {392function get_distroseries_nodes() {
393 return request_build_overlay.form_node.all(393 return request_build_overlay.form_node.all(
394 "label[for^='field.distros.']");394 "label[for^='field.distroseries.']");
395}395}
396396
397var DISABLED_DISTROSERIES_CHECKBOX_HTML =397var DISABLED_DISTROSERIES_CHECKBOX_HTML =
398398
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2011-03-18 06:47:15 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2011-03-22 02:47:23 +0000
@@ -187,6 +187,12 @@
187 def recipe_text(self):187 def recipe_text(self):
188 return self.builder_recipe.get_recipe_text()188 return self.builder_recipe.get_recipe_text()
189189
190 def updateSeries(self, distroseries):
191 if distroseries != self.distroseries:
192 self.distroseries.clear()
193 for distroseries_item in distroseries:
194 self.distroseries.add(distroseries_item)
195
190 @staticmethod196 @staticmethod
191 def new(registrant, owner, name, recipe, description,197 def new(registrant, owner, name, recipe, description,
192 distroseries=None, daily_build_archive=None, build_daily=False,198 distroseries=None, daily_build_archive=None, build_daily=False,
193199
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-03-02 01:47:19 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-03-22 02:47:23 +0000
@@ -118,15 +118,8 @@
118 <dt>Daily build archive:</dt>118 <dt>Daily build archive:</dt>
119 <dd tal:content="structure view/archive_picker"/>119 <dd tal:content="structure view/archive_picker"/>
120 </dl>120 </dl>
121121 <dl id="distroseries">
122 <dl id="distros">122 <tal:distroseries tal:replace="structure view/distroseries_widget"/>
123 <dt>Distribution series:</dt>
124 <dd>
125 <ul>
126 <li tal:repeat="curseries context/distroseries"
127 tal:content="structure curseries/fmt:link" />
128 </ul>
129 </dd>
130 </dl>123 </dl>
131 </div>124 </div>
132 </div>125 </div>
133126
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
--- lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-02-14 01:48:57 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-03-22 02:47:23 +0000
@@ -99,7 +99,7 @@
99 </tal:widget>99 </tal:widget>
100 </tal:create-ppa>100 </tal:create-ppa>
101101
102 <tal:widget define="widget nocall:view/widgets/distros">102 <tal:widget define="widget nocall:view/widgets/distroseries">
103 <metal:block use-macro="context/@@launchpad_form/widget_row" />103 <metal:block use-macro="context/@@launchpad_form/widget_row" />
104 </tal:widget>104 </tal:widget>
105 <tal:widget define="widget nocall:view/widgets/recipe_text">105 <tal:widget define="widget nocall:view/widgets/recipe_text">
106106
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-request-builds.pt'
--- lib/lp/code/templates/sourcepackagerecipe-request-builds.pt 2010-03-31 19:27:07 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-request-builds.pt 2011-03-22 02:47:23 +0000
@@ -16,7 +16,7 @@
16 <tal:widget define="widget nocall:view/widgets/archive">16 <tal:widget define="widget nocall:view/widgets/archive">
17 <metal:block use-macro="context/@@launchpad_form/widget_row" />17 <metal:block use-macro="context/@@launchpad_form/widget_row" />
18 </tal:widget>18 </tal:widget>
19 <tal:widget define="widget nocall:view/widgets/distros">19 <tal:widget define="widget nocall:view/widgets/distroseries">
20 <metal:block use-macro="context/@@launchpad_form/widget_row" />20 <metal:block use-macro="context/@@launchpad_form/widget_row" />
21 </tal:widget>21 </tal:widget>
22 </table>22 </table>
2323
=== added file 'lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py'
--- lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/windmill/tests/test_recipe_inline_distroseries_edit.py 2011-03-22 02:47:23 +0000
@@ -0,0 +1,71 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for requesting recipe builds."""
5
6__metaclass__ = type
7__all__ = []
8
9import transaction
10
11from zope.component import getUtility
12from storm.store import Store
13
14from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
15from lp.testing import WindmillTestCase
16from lp.testing.windmill.constants import (
17 FOR_ELEMENT,
18 PAGE_LOAD,
19 )
20from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
21from lp.code.windmill.testing import CodeWindmillLayer
22
23
24class TestRecipeEdit(WindmillTestCase):
25 """Test recipe editing with inline widgets."""
26
27 layer = CodeWindmillLayer
28 suite_name = "Request recipe build"
29
30 def test_inline_distroseries_edit(self):
31 """Test that inline editing of distroseries works."""
32
33 chef = self.factory.makePerson(
34 displayname='Master Chef', name='chef', password='test',
35 email="chef@example.com")
36 recipe = self.factory.makeSourcePackageRecipe(owner=chef)
37 transaction.commit()
38
39 client, start_url = self.getClientFor(recipe, user=chef)
40 client.waits.forElement(
41 id=u'edit-distroseries-items', timeout=PAGE_LOAD)
42
43 # Edit the distro series.
44 client.click(jquery=u'("#edit-distroseries-btn")[0]')
45 client.waits.forElement(
46 jquery=u'("#edit-distroseries-save")',
47 timeout=FOR_ELEMENT)
48 # Click the checkbox to select the first distro series
49 client.click(name=u'field.distroseries.0')
50 client.waits.forElement(
51 jquery=u"('[name=\"field.distroseries.0\"][checked=\"checked\"]')",
52 timeout=FOR_ELEMENT)
53 # Save it
54 client.click(jquery=u'("#edit-distroseries-save")[0]')
55
56 # Wait for the the new one that is added.
57 client.waits.forElement(
58 jquery=u"('#edit-distroseries-items ul li a')[0]",
59 timeout=FOR_ELEMENT)
60
61 # Check that the new data was saved.
62 transaction.commit()
63 hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
64 store = Store.of(recipe)
65 saved_recipe = store.find(
66 SourcePackageRecipe,
67 SourcePackageRecipe.name==recipe.name).one()
68 self.assertEqual(len(list(saved_recipe.distroseries)), 2)
69 distroseries=sorted(
70 saved_recipe.distroseries, key=lambda ds: ds.displayname)
71 self.assertEqual(distroseries[0], hoary)