Merge lp:~thumper/launchpad/daily-ajax into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12377
Proposed branch: lp:~thumper/launchpad/daily-ajax
Merge into: lp:launchpad
Diff against target: 680 lines (+368/-43)
15 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+7/-1)
lib/lp/app/browser/lazrjs.py (+88/-15)
lib/lp/app/doc/lazr-js-widgets.txt (+58/-0)
lib/lp/app/javascript/choice.js (+37/-0)
lib/lp/app/templates/boolean-choice-widget.pt (+18/-0)
lib/lp/app/templates/launchpad-form.pt (+14/-1)
lib/lp/code/browser/sourcepackagerecipe.py (+13/-1)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+8/-8)
lib/lp/code/help/recipe-build-frequency.html (+41/-0)
lib/lp/code/interfaces/sourcepackagerecipe.py (+2/-2)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+6/-7)
lib/lp/code/templates/sourcepackagerecipe-new.pt (+2/-1)
lib/lp/code/windmill/tests/test_recipe_index.py (+46/-0)
lib/lp/testing/__init__.py (+18/-1)
lib/lp/testing/windmill/lpuser.py (+10/-6)
To merge this branch: bzr merge lp:~thumper/launchpad/daily-ajax
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
Robert Collins (community) Approve
William Grant code* Approve
Review via email: mp+49295@code.launchpad.net

Commit message

[r=deryck,lifeless,wgrant][bug=673530][incr] Add a popup choice widget for the build frequency.

Description of the change

The primary motivation of this branch is to add a simple popup
chooser for the build frequency. I did this by creating a
BooleanChoiceWidget.

lib/canonical/launchpad/icing/style-3-0.css.in
 - Since the value string of the choice is clickable, we should
   make the cursor change, and underline it as the user hovers

The lazr-js-widgets documentation is updated to cover the new
widget.

I also added an in-page help link about the build schedule.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

This looks good. Just a few comments:

 - In style-3-0.css.in, there is a space missing after "cursor:".
 - In the help page: "before the 24 period is up" needs a s/24 period/24 hour period/. It also seems slightly repetitive, but it may be needed for clarity.
 - You've changed "Built daily" to "Build daily". Was that deliberate? I slightly prefer the former, but am not really fussed.
 - This UI is very different from the "Automatically build each day, if the source has changed" checkbox when creating a new recipe. Can we use the same option buttons and help there too?

review: Approve (code*)
Revision history for this message
Robert Collins (lifeless) wrote :

@wgrant, @thumper - Theres an odd structure here:

The mixin looks like it could just depend on the Editablewidgetbase protocol; I wouldn't use a mixin at this point, just pull it up to the base class (and pull the self.tag setting up too).

Have a think about this, but don't feel you need to tell me what you choose to do.

review: Approve
Revision history for this message
Deryck Hodge (deryck) wrote :

Hi, all.

There are some problems with this widget and Windmill test. The widget doesn't show a spinner while it's doing XHR work, and the Windmill test shouldn't be using sleep or xpath for this simple of a test. I'm having a poke at this branch briefly to see if I can just provide a diff to show how this should work (in the hopes that that is helpful and not intrusive ;)) and will reply back when I come up with something.

Cheers,
deryck

review: Needs Fixing (javascript code)
Revision history for this message
Deryck Hodge (deryck) wrote :

Here's a patch that fixes this up some. It:

* ensures there is a spinner during the XHR work
* avoids sleep during the test
* avoids xpath
* always use a timeout on waits.forElement
* doesn't rely on a page reload to confirm data

This should be a pretty stable test now, and it's clearer to read.
It's also a little bit faster than the earlier version on my system
(around 3-4 seconds).

If you have questions ping me about it.

Cheers,
deryck

=== modified file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js 2011-02-10 01:57:55 +0000
+++ lib/lp/app/javascript/choice.js 2011-02-11 16:46:13 +0000
@@ -14,6 +14,22 @@
14 cfg: {14 cfg: {
15 patch: attribute,15 patch: attribute,
16 resource: resource_uri}});16 resource: resource_uri}});
17 // ChoiceSource makes assumptions about HTML in lazr-js
18 // that don't hold true here, so we need to do our own
19 // spinner icon and clear it when finished.
20 Y.after(function() {
21 var icon = this.get('editicon');
22 icon.removeClass('edit');
23 icon.addClass('update-in-progress-message');
24 icon.setStyle('position', 'relative');
25 icon.setStyle('bottom', '2px');
26 }, widget, '_uiSetWaiting');
27 Y.after(function() {
28 var icon = this.get('editicon');
29 icon.removeClass('update-in-progress-message');
30 icon.addClass('edit');
31 icon.setStyle('bottom', '0px');
32 }, widget, '_uiClearWaiting');
17 widget.render();33 widget.render();
18};34};
1935
2036
=== modified file 'lib/lp/code/windmill/tests/test_recipe_index.py'
--- lib/lp/code/windmill/tests/test_recipe_index.py 2011-02-11 01:49:55 +0000
+++ lib/lp/code/windmill/tests/test_recipe_index.py 2011-02-11 18:08:58 +0000
@@ -1,20 +1,19 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for branch statuses."""4"""Tests for recipe index pages."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = []7__all__ = []
88
9import transaction9import transaction
10import unittest10
1111from storm.store import Store
12
13from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
12from lp.code.windmill.testing import CodeWindmillLayer14from lp.code.windmill.testing import CodeWindmillLayer
13from lp.testing import WindmillTestCase15from lp.testing import WindmillTestCase
14from lp.testing.windmill.constants import (16from lp.testing.windmill.constants import FOR_ELEMENT
15 PAGE_LOAD,
16 SLEEP,
17 )
1817
1918
20class TestRecipeSetDaily(WindmillTestCase):19class TestRecipeSetDaily(WindmillTestCase):
@@ -23,9 +22,6 @@
23 layer = CodeWindmillLayer22 layer = CodeWindmillLayer
24 suite_name = "Recipe daily build flag setting"23 suite_name = "Recipe daily build flag setting"
2524
26 BUILD_DAILY_TEXT = u'//span[@id="edit-build_daily"]/span[@class="value"]'
27 BUILD_DAILY_POPUP = u'//div[contains(@class, "yui3-ichoicelist-content")]'
28
29 def test_inline_recipe_daily_build(self):25 def test_inline_recipe_daily_build(self):
30 eric = self.factory.makePerson(26 eric = self.factory.makePerson(
31 name="eric", displayname="Eric the Viking", password="test",27 name="eric", displayname="Eric the Viking", password="test",
@@ -34,15 +30,17 @@
34 transaction.commit()30 transaction.commit()
3531
36 client, start_url = self.getClientFor(recipe, user=eric)32 client, start_url = self.getClientFor(recipe, user=eric)
37 client.click(xpath=self.BUILD_DAILY_TEXT)33 client.click(id=u'edit-build_daily')
38 client.waits.forElement(xpath=self.BUILD_DAILY_POPUP)34 client.waits.forElement(
35 classname=u'yui3-ichoicelist-content', timeout=FOR_ELEMENT)
39 client.click(link=u'Build daily')36 client.click(link=u'Build daily')
40 client.waits.sleep(milliseconds=SLEEP)37 client.waits.forElement(
41 client.asserts.assertText(38 jquery=u'("div#edit-build_daily a.editicon.sprite.edit")',
42 xpath=self.BUILD_DAILY_TEXT, validator=u'Build daily')39 timeout=FOR_ELEMENT)
40 client.asserts.assertTextIn(
41 id=u'edit-build_daily', validator=u'Build daily')
4342
44 # Reload the page and make sure the change has stuck.43 transaction.commit()
45 client.open(url=start_url)44 freshly_fetched_recipe = Store.of(recipe).find(
46 client.waits.forPageLoad(timeout=PAGE_LOAD)45 SourcePackageRecipe, SourcePackageRecipe.id == recipe.id).one()
47 client.asserts.assertText(46 self.assertTrue(freshly_fetched_recipe.build_daily)
48 xpath=self.BUILD_DAILY_TEXT, validator=u'Build daily')
Revision history for this message
Tim Penhey (thumper) wrote :

On Fri, 11 Feb 2011 19:17:53 you wrote:
> Review: Approve
> @wgrant, @thumper - Theres an odd structure here:
>
> The mixin looks like it could just depend on the Editablewidgetbase
> protocol; I wouldn't use a mixin at this point, just pull it up to the
> base class (and pull the self.tag setting up too).
>
> Have a think about this, but don't feel you need to tell me what you choose
> to do.

I did think about this, but the multiline editor does not need it, so I
decided not to put it into the EditableWidgetBase.

Revision history for this message
Tim Penhey (thumper) wrote :

On Sat, 12 Feb 2011 07:55:32 you wrote:
> Here's a patch that fixes this up some. It:
>
> * ensures there is a spinner during the XHR work
> * avoids sleep during the test
> * avoids xpath
> * always use a timeout on waits.forElement
> * doesn't rely on a page reload to confirm data

Thanks for this Deryck. I've included your patch. We need to make sure that
more Javascript reviewers understand the best way to write the windmill tests.

Revision history for this message
Deryck Hodge (deryck) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-10 23:30:53 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-14 07:50:33 +0000
@@ -963,11 +963,17 @@
963 /* The js-action class is also used for non-links, for example, with963 /* The js-action class is also used for non-links, for example, with
964 expand/collapse sections. */964 expand/collapse sections. */
965 color: #093;965 color: #093;
966 cursor:pointer;966 cursor: pointer;
967 }967 }
968.widget-hd.js-action:hover {968.widget-hd.js-action:hover {
969 text-decoration: underline;969 text-decoration: underline;
970 }970 }
971
972.yui3-ichoicesource-content .value:hover {
973 text-decoration: underline;
974 cursor: pointer;
975 }
976
971.error.message, .warning.message, .informational.message {977.error.message, .warning.message, .informational.message {
972 border: solid #666;978 border: solid #666;
973 border-width: 1px 2px 2px 1px;979 border-width: 1px 2px 2px 1px;
974980
=== modified file 'lib/lp/app/browser/lazrjs.py'
--- lib/lp/app/browser/lazrjs.py 2011-01-28 01:09:35 +0000
+++ lib/lp/app/browser/lazrjs.py 2011-02-14 07:50:33 +0000
@@ -5,6 +5,7 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'BooleanChoiceWidget',
8 'InlineEditPickerWidget',9 'InlineEditPickerWidget',
9 'standard_text_html_representation',10 'standard_text_html_representation',
10 'TextAreaEditorWidget',11 'TextAreaEditorWidget',
@@ -61,6 +62,7 @@
61 if mutator_info is not None:62 if mutator_info is not None:
62 mutator_method, mutator_extra = mutator_info63 mutator_method, mutator_extra = mutator_info
63 self.mutator_method_name = mutator_method.__name__64 self.mutator_method_name = mutator_method.__name__
65 self.json_attribute = simplejson.dumps(self.api_attribute)
6466
65 @property67 @property
66 def resource_uri(self):68 def resource_uri(self):
@@ -90,19 +92,27 @@
90 return False92 return False
9193
9294
93class TextWidgetBase(WidgetBase):95class EditableWidgetBase(WidgetBase):
96 """Adds an edit_url property to WidgetBase."""
97
98 def __init__(self, context, exported_field, content_box_id,
99 edit_view, edit_url):
100 super(EditableWidgetBase, self).__init__(
101 context, exported_field, content_box_id)
102 if edit_url is None:
103 edit_url = canonical_url(self.context, view_name=edit_view)
104 self.edit_url = edit_url
105
106
107class TextWidgetBase(EditableWidgetBase):
94 """Abstract base for the single and multiline text editor widgets."""108 """Abstract base for the single and multiline text editor widgets."""
95109
96 def __init__(self, context, exported_field, title, content_box_id,110 def __init__(self, context, exported_field, title, content_box_id,
97 edit_view, edit_url):111 edit_view, edit_url):
98 super(TextWidgetBase, self).__init__(112 super(TextWidgetBase, self).__init__(
99 context, exported_field, content_box_id)113 context, exported_field, content_box_id, edit_view, edit_url)
100 if edit_url is None:
101 edit_url = canonical_url(self.context, view_name=edit_view)
102 self.edit_url = edit_url
103 self.accept_empty = simplejson.dumps(self.optional_field)114 self.accept_empty = simplejson.dumps(self.optional_field)
104 self.title = title115 self.title = title
105 self.json_attribute = simplejson.dumps(self.api_attribute)
106 self.widget_css_selector = simplejson.dumps('#' + self.content_box_id)116 self.widget_css_selector = simplejson.dumps('#' + self.content_box_id)
107117
108 @property118 @property
@@ -110,7 +120,19 @@
110 return simplejson.dumps(self.resource_uri + '/' + self.api_attribute)120 return simplejson.dumps(self.resource_uri + '/' + self.api_attribute)
111121
112122
113class TextLineEditorWidget(TextWidgetBase):123class DefinedTagMixin:
124 """Mixin class to define open and closing tags."""
125
126 @property
127 def open_tag(self):
128 return '<%s id="%s">' % (self.tag, self.content_box_id)
129
130 @property
131 def close_tag(self):
132 return '</%s>' % self.tag
133
134
135class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
114 """Wrapper for the lazr-js inlineedit/editor.js widget."""136 """Wrapper for the lazr-js inlineedit/editor.js widget."""
115137
116 __call__ = ViewPageTemplateFile('../templates/text-line-editor.pt')138 __call__ = ViewPageTemplateFile('../templates/text-line-editor.pt')
@@ -146,14 +168,6 @@
146 self.width = simplejson.dumps(width)168 self.width = simplejson.dumps(width)
147169
148 @property170 @property
149 def open_tag(self):
150 return '<%s id="%s">' % (self.tag, self.content_box_id)
151
152 @property
153 def close_tag(self):
154 return '</%s>' % self.tag
155
156 @property
157 def value(self):171 def value(self):
158 text = getattr(self.context, self.attribute_name, self.default_text)172 text = getattr(self.context, self.attribute_name, self.default_text)
159 if text is None:173 if text is None:
@@ -333,3 +347,62 @@
333 return ''347 return ''
334 nomail = FormattersAPI(value).obfuscate_email()348 nomail = FormattersAPI(value).obfuscate_email()
335 return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text)349 return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text)
350
351
352class BooleanChoiceWidget(EditableWidgetBase, DefinedTagMixin):
353 """A ChoiceEdit for a boolean field."""
354
355 __call__ = ViewPageTemplateFile('../templates/boolean-choice-widget.pt')
356
357 def __init__(self, context, exported_field,
358 tag, false_text, true_text, prefix=None,
359 edit_view="+edit", edit_url=None,
360 content_box_id=None, header='Select an item'):
361 """Create a widget wrapper.
362
363 :param context: The object that is being edited.
364 :param exported_field: The attribute being edited. This should be
365 a field from an interface of the form ISomeInterface['fieldname']
366 :param tag: The HTML tag to use.
367 :param false_text: The string to show for a false value.
368 :param true_text: The string to show for a true value.
369 :param prefix: Optional text to show before the value.
370 :param edit_view: The view name to use to generate the edit_url if
371 one is not specified.
372 :param edit_url: The URL to use for editing when the user isn't logged
373 in and when JS is off. Defaults to the edit_view on the context.
374 :param content_box_id: The HTML id to use for this widget. Automatically
375 generated if this is not provided.
376 :param header: The large text at the top of the choice popup.
377 """
378 super(BooleanChoiceWidget, self).__init__(
379 context, exported_field, content_box_id, edit_view, edit_url)
380 self.header = header
381 self.tag = tag
382 self.prefix = prefix
383 self.true_text = true_text
384 self.false_text = false_text
385 self.current_value = getattr(self.context, self.attribute_name)
386
387 @property
388 def value(self):
389 if self.current_value:
390 return self.true_text
391 else:
392 return self.false_text
393
394 @property
395 def config(self):
396 return dict(
397 contentBox='#'+self.content_box_id,
398 value=self.current_value,
399 title=self.header,
400 items=[
401 dict(name=self.true_text, value=True, style='', help='',
402 disabled=False),
403 dict(name=self.false_text, value=False, style='', help='',
404 disabled=False)])
405
406 @property
407 def json_config(self):
408 return simplejson.dumps(self.config)
336409
=== modified file 'lib/lp/app/doc/lazr-js-widgets.txt'
--- lib/lp/app/doc/lazr-js-widgets.txt 2011-01-28 00:54:11 +0000
+++ lib/lp/app/doc/lazr-js-widgets.txt 2011-02-14 07:50:33 +0000
@@ -268,3 +268,61 @@
268268
269If the field is optional, a "Remove" link is shown. The "Remove" text is269If the field is optional, a "Remove" link is shown. The "Remove" text is
270customizable thought the "remove_button_text" parameter.270customizable thought the "remove_button_text" parameter.
271
272
273BooleanChoiceWidget
274-------------------
275
276This widget provides a simple popup with two options for the user to choose
277from.
278
279 >>> from lp.app.browser.lazrjs import BooleanChoiceWidget
280
281As with the other widgets, this one requires a context object and a Bool type
282field. The rendering of the widget hooks up to the lazr ChoiceSource with the
283standard patch plugin.
284
285The surrounding tag is customisable, and a prefix may be given. The prefix is
286passed through to the ChoiceSource and is rendered as part of the widget, but
287isn't updated when the value changes.
288
289If the user does not have edit rights, the widget just renders the text based
290on the current value of the field on the object:
291
292 >>> login(ANONYMOUS)
293 >>> from lp.registry.interfaces.person import IPerson
294 >>> hide_email = IPerson['hide_email_addresses']
295 >>> widget = BooleanChoiceWidget(
296 ... eric, hide_email, 'span',
297 ... false_text="Don't hide it",
298 ... true_text="Keep it secret",
299 ... prefix="My email: ")
300 >>> print widget()
301 <span id="edit-hide_email_addresses">
302 My email: <span class="value">Don't hide it</span>
303 </span>
304
305If the user has edit rights, an edit icon is rendered and some javascript is
306rendered to hook up the widget.
307
308 >>> login_person(eric)
309 >>> print widget()
310 <span id="edit-hide_email_addresses">
311 My email: <span class="value">Don't hide it</span>
312 <span>
313 &nbsp;
314 <a class="editicon sprite edit"
315 href="http://launchpad.dev/~eric/+edit"></a>
316 </span>
317 </span>
318 <script>
319 LPS.use('lp.app.choice', function(Y) {
320 ...
321 </script>
322
323
324Changing the edit link
325**********************
326
327The edit link can be changed in exactly the same way as for the
328TextLineEditorWidget above.
271329
=== added file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/choice.js 2011-02-14 07:50:33 +0000
@@ -0,0 +1,37 @@
1YUI.add('lp.app.choice', function(Y) {
2
3var namespace = Y.namespace('lp.app.choice');
4
5namespace.addBinaryChoice = function(config, resource_uri, attribute) {
6
7 if (Y.UA.ie) {
8 return;
9 }
10
11 var widget = new Y.ChoiceSource(config);
12 widget.plug({
13 fn: Y.lp.client.plugins.PATCHPlugin,
14 cfg: {
15 patch: attribute,
16 resource: resource_uri}});
17 // ChoiceSource makes assumptions about HTML in lazr-js
18 // that don't hold true here, so we need to do our own
19 // spinner icon and clear it when finished.
20 Y.after(function() {
21 var icon = this.get('editicon');
22 icon.removeClass('edit');
23 icon.addClass('update-in-progress-message');
24 icon.setStyle('position', 'relative');
25 icon.setStyle('bottom', '2px');
26 }, widget, '_uiSetWaiting');
27 Y.after(function() {
28 var icon = this.get('editicon');
29 icon.removeClass('update-in-progress-message');
30 icon.addClass('edit');
31 icon.setStyle('bottom', '0px');
32 }, widget, '_uiClearWaiting');
33 widget.render();
34};
35
36
37}, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins"]});
038
=== added file 'lib/lp/app/templates/boolean-choice-widget.pt'
--- lib/lp/app/templates/boolean-choice-widget.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/templates/boolean-choice-widget.pt 2011-02-14 07:50:33 +0000
@@ -0,0 +1,18 @@
1<tal:open-tag replace="structure view/open_tag"/>
2<tal:prefix replace="view/prefix"/><span class="value" tal:content="view/value">Unset</span>
3 <span tal:condition="view/can_write">
4 &nbsp;
5 <a tal:attributes="href view/edit_url"
6 class="editicon sprite edit"></a>
7 </span>
8<tal:close-tag replace="structure view/close_tag"/>
9
10<script tal:condition="view/can_write"
11 tal:content="structure string:
12LPS.use('lp.app.choice', function(Y) {
13 Y.lp.app.choice.addBinaryChoice(
14 ${view/json_config},
15 ${view/json_resource_uri},
16 ${view/json_attribute});
17});
18"/>
019
=== modified file 'lib/lp/app/templates/launchpad-form.pt'
--- lib/lp/app/templates/launchpad-form.pt 2010-11-26 02:01:47 +0000
+++ lib/lp/app/templates/launchpad-form.pt 2011-02-14 07:50:33 +0000
@@ -104,7 +104,8 @@
104 error python:view.getFieldError(field_name);104 error python:view.getFieldError(field_name);
105 error_class python:error and 'error' or None;105 error_class python:error and 'error' or None;
106 show_optional python:view.showOptionalMarker(field_name);106 show_optional python:view.showOptionalMarker(field_name);
107 widget_class widget/widget_class|nothing">107 widget_class widget/widget_class|nothing;
108 widget_help_link widget_help_link|nothing">
108 <tal:is-visible condition="widget/visible">109 <tal:is-visible condition="widget/visible">
109 <tr110 <tr
110 tal:condition="python: view.isSingleLineLayout(field_name)"111 tal:condition="python: view.isSingleLineLayout(field_name)"
@@ -120,6 +121,12 @@
120 </tal:block>121 </tal:block>
121 <div>122 <div>
122 <input tal:replace="structure widget" />123 <input tal:replace="structure widget" />
124 <tal:help-link condition="widget_help_link">
125 <a tal:attributes="href widget_help_link"
126 target="help" class="sprite maybe">
127 &nbsp;<span class="invisible-link">Tag help</span>
128 </a>
129 </tal:help-link>
123 </div>130 </div>
124 <div class="message" tal:condition="error"131 <div class="message" tal:condition="error"
125 tal:content="structure error">Error message</div>132 tal:content="structure error">Error message</div>
@@ -160,6 +167,12 @@
160 <input type="checkbox" tal:replace="structure widget" />167 <input type="checkbox" tal:replace="structure widget" />
161 <label tal:attributes="for widget/name"168 <label tal:attributes="for widget/name"
162 tal:content="widget/label">Label</label>169 tal:content="widget/label">Label</label>
170 <tal:help-link condition="widget_help_link">
171 <a tal:attributes="href widget_help_link"
172 target="help" class="sprite maybe">
173 &nbsp;<span class="invisible-link">Tag help</span>
174 </a>
175 </tal:help-link>
163 <div176 <div
164 tal:condition="error"177 tal:condition="error"
165 tal:content="structure error"178 tal:content="structure error"
166179
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-02-03 05:23:55 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-14 07:50:33 +0000
@@ -69,7 +69,10 @@
69 LaunchpadFormView,69 LaunchpadFormView,
70 render_radio_widget_part,70 render_radio_widget_part,
71 )71 )
72from lp.app.browser.lazrjs import InlineEditPickerWidget72from lp.app.browser.lazrjs import (
73 BooleanChoiceWidget,
74 InlineEditPickerWidget,
75 )
73from lp.app.browser.tales import format_link76from lp.app.browser.tales import format_link
74from lp.app.widgets.itemswidgets import (77from lp.app.widgets.itemswidgets import (
75 LabeledMultiCheckBoxWidget,78 LabeledMultiCheckBoxWidget,
@@ -260,6 +263,15 @@
260 header='Change daily build archive',263 header='Change daily build archive',
261 step_title='Select a PPA')264 step_title='Select a PPA')
262265
266 @property
267 def daily_build_widget(self):
268 return BooleanChoiceWidget(
269 self.context, ISourcePackageRecipe['build_daily'],
270 tag='span',
271 false_text='Built on request',
272 true_text='Built daily',
273 header='Change build schedule')
274
263275
264class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):276class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
265 """A view for requesting builds of a SourcePackageRecipe."""277 """A view for requesting builds of a SourcePackageRecipe."""
266278
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-03 05:23:55 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-14 07:50:33 +0000
@@ -218,7 +218,7 @@
218 browser.getControl(name='field.name').value = 'daily'218 browser.getControl(name='field.name').value = 'daily'
219 browser.getControl('Description').value = 'Make some food!'219 browser.getControl('Description').value = 'Make some food!'
220 browser.getControl('Secret Squirrel').click()220 browser.getControl('Secret Squirrel').click()
221 browser.getControl('Automatically build each day').click()221 browser.getControl('Built daily').click()
222 browser.getControl('Create Recipe').click()222 browser.getControl('Create Recipe').click()
223223
224 pattern = """\224 pattern = """\
@@ -229,7 +229,7 @@
229 Make some food!229 Make some food!
230230
231 Recipe information231 Recipe information
232 Build schedule: Built daily232 Build schedule: Tag help Built daily
233 Owner: Master Chef Edit233 Owner: Master Chef Edit
234 Base branch: lp://dev/~chef/ratatouille/veggies234 Base branch: lp://dev/~chef/ratatouille/veggies
235 Debian version: {debupstream}-0~{revno}235 Debian version: {debupstream}-0~{revno}
@@ -281,7 +281,7 @@
281 browser.getControl(name='field.name').value = 'daily'281 browser.getControl(name='field.name').value = 'daily'
282 browser.getControl('Description').value = 'Make some food!'282 browser.getControl('Description').value = 'Make some food!'
283 browser.getControl('Secret Squirrel').click()283 browser.getControl('Secret Squirrel').click()
284 browser.getControl('Automatically build each day').click()284 browser.getControl('Built daily').click()
285 browser.getControl('Other').click()285 browser.getControl('Other').click()
286 browser.getControl(name='field.owner.owner').displayValue = [286 browser.getControl(name='field.owner.owner').displayValue = [
287 'Good Chefs']287 'Good Chefs']
@@ -427,7 +427,7 @@
427 browser.getControl(name='field.name').value = 'daily'427 browser.getControl(name='field.name').value = 'daily'
428 browser.getControl('Description').value = 'Make some food!'428 browser.getControl('Description').value = 'Make some food!'
429429
430 browser.getControl('Automatically build each day').click()430 browser.getControl('Built daily').click()
431 browser.getControl('Create Recipe').click()431 browser.getControl('Create Recipe').click()
432 self.assertEqual(432 self.assertEqual(
433 'You must specify at least one series for daily builds.',433 'You must specify at least one series for daily builds.',
@@ -755,7 +755,7 @@
755 This is stuff755 This is stuff
756756
757 Recipe information757 Recipe information
758 Build schedule: Built on request758 Build schedule: Tag help Built on request
759 Owner: Master Chef Edit759 Owner: Master Chef Edit
760 Base branch: lp://dev/~chef/ratatouille/meat760 Base branch: lp://dev/~chef/ratatouille/meat
761 Debian version: {debupstream}-0~{revno}761 Debian version: {debupstream}-0~{revno}
@@ -815,7 +815,7 @@
815 This is stuff815 This is stuff
816816
817 Recipe information817 Recipe information
818 Build schedule: Built on request818 Build schedule: Tag help Built on request
819 Owner: Master Chef Edit819 Owner: Master Chef Edit
820 Base branch: lp://dev/~chef/ratatouille/meat820 Base branch: lp://dev/~chef/ratatouille/meat
821 Debian version: {debupstream}-0~{revno}821 Debian version: {debupstream}-0~{revno}
@@ -955,7 +955,7 @@
955 This is stuff955 This is stuff
956956
957 Recipe information957 Recipe information
958 Build schedule: Built on request958 Build schedule: Tag help Built on request
959 Owner: Master Chef Edit959 Owner: Master Chef Edit
960 Base branch: lp://dev/~chef/ratatouille/meat960 Base branch: lp://dev/~chef/ratatouille/meat
961 Debian version: {debupstream}-0~{revno}961 Debian version: {debupstream}-0~{revno}
@@ -1098,7 +1098,7 @@
1098 This recipe .*changes.1098 This recipe .*changes.
10991099
1100 Recipe information1100 Recipe information
1101 Build schedule: Built on request1101 Build schedule: Tag help Built on request
1102 Owner: Master Chef Edit1102 Owner: Master Chef Edit
1103 Base branch: lp://dev/~chef/chocolate/cake1103 Base branch: lp://dev/~chef/chocolate/cake
1104 Debian version: {debupstream}-0~{revno}1104 Debian version: {debupstream}-0~{revno}
11051105
=== added file 'lib/lp/code/help/recipe-build-frequency.html'
--- lib/lp/code/help/recipe-build-frequency.html 1970-01-01 00:00:00 +0000
+++ lib/lp/code/help/recipe-build-frequency.html 2011-02-14 07:50:33 +0000
@@ -0,0 +1,41 @@
1<html>
2 <head>
3 <title>Source package recipe build schedule</title>
4 <link rel="stylesheet" type="text/css"
5 href="/+icing/yui/cssreset/reset.css" />
6 <link rel="stylesheet" type="text/css"
7 href="/+icing/yui/cssfonts/fonts.css" />
8 <link rel="stylesheet" type="text/css"
9 href="/+icing/yui/cssbase/base.css" />
10 <style type="text/css">
11 dt { font-weight: bold }
12 dd p { margin-bottom: 0.5em }
13 </style>
14 </head>
15 <body>
16 <h1>Source package recipe build schedule</h1>
17
18 <p>There are two options for when recipes get built:</p>
19 <dl>
20 <dt>Built daily</dt>
21 <dd>
22 <p>A build will be scheduled automatically once a change in any
23 of the branches used in the recipe is detected.</p>
24 <p>If there has been a build of the recipe within the previous
25 24 hours into the daily build PPA, the build will not be scheduled
26 until 24 hours since the last build into the daily build PPA.</p>
27 <p>If the recipe had been built within the last 24 hours into a
28 different PPA using the "Request build" action, this will not
29 delay the daily build.</p>
30 <p>If you really want the build to happen before the 24 hour period is up,
31 you can use the "Request build" action.</p>
32 </dd>
33 <dt>Built on request</dt>
34 <dd>
35 <p>Builds of the recipe have to be manually requested using the
36 "Request build" action.</p>
37 </dd>
38 </dl>
39
40 </body>
41</html>
042
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-01-13 03:53:53 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-14 07:50:33 +0000
@@ -171,8 +171,8 @@
171 " build a source package for"),171 " build a source package for"),
172 readonly=False)172 readonly=False)
173 build_daily = exported(Bool(173 build_daily = exported(Bool(
174 title=_("Automatically build each day, if the source has changed"),174 title=_("Built daily"),
175 description=_("You can manually request a build at any time.")))175 description=_("Automatically build each day, if the source has changed.")))
176176
177 name = exported(TextLine(177 name = exported(TextLine(
178 title=_("Name"), required=True,178 title=_("Name"), required=True,
179179
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-01-20 20:35:32 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-02-14 07:50:33 +0000
@@ -44,13 +44,12 @@
44 <h2>Recipe information</h2>44 <h2>Recipe information</h2>
45 <div class="two-column-list">45 <div class="two-column-list">
46 <dl id="build_daily">46 <dl id="build_daily">
47 <dt>Build schedule:</dt>47 <dt>Build schedule:
48 <dd tal:condition="context/build_daily">48 <a href="/+help/recipe-build-frequency.html" target="help" class="sprite maybe">
49 Built daily49 &nbsp;<span class="invisible-link">Tag help</span>
50 </dd>50 </a>
51 <dd tal:condition="not:context/build_daily">51 </dt>
52 Built on request52 <dd tal:content="structure view/daily_build_widget"/>
53 </dd>
54 </dl>53 </dl>
5554
56 <dl id="owner">55 <dl id="owner">
5756
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
--- lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-01-25 04:39:16 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-02-14 07:50:33 +0000
@@ -53,7 +53,8 @@
53 <tal:widget define="widget nocall:view/widgets/owner">53 <tal:widget define="widget nocall:view/widgets/owner">
54 <metal:block use-macro="context/@@launchpad_form/widget_row" />54 <metal:block use-macro="context/@@launchpad_form/widget_row" />
55 </tal:widget>55 </tal:widget>
56 <tal:widget define="widget nocall:view/widgets/build_daily">56 <tal:widget define="widget nocall:view/widgets/build_daily;
57 widget_help_link string:/+help/recipe-build-frequency.html">
57 <metal:block use-macro="context/@@launchpad_form/widget_row" />58 <metal:block use-macro="context/@@launchpad_form/widget_row" />
58 </tal:widget>59 </tal:widget>
5960
6061
=== added file 'lib/lp/code/windmill/tests/test_recipe_index.py'
--- lib/lp/code/windmill/tests/test_recipe_index.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/windmill/tests/test_recipe_index.py 2011-02-14 07:50:33 +0000
@@ -0,0 +1,46 @@
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 recipe index pages."""
5
6__metaclass__ = type
7__all__ = []
8
9import transaction
10
11from storm.store import Store
12
13from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
14from lp.code.windmill.testing import CodeWindmillLayer
15from lp.testing import WindmillTestCase
16from lp.testing.windmill.constants import FOR_ELEMENT
17
18
19class TestRecipeSetDaily(WindmillTestCase):
20 """Test setting the daily build flag."""
21
22 layer = CodeWindmillLayer
23 suite_name = "Recipe daily build flag setting"
24
25 def test_inline_recipe_daily_build(self):
26 eric = self.factory.makePerson(
27 name="eric", displayname="Eric the Viking", password="test",
28 email="eric@example.com")
29 recipe = self.factory.makeSourcePackageRecipe(owner=eric)
30 transaction.commit()
31
32 client, start_url = self.getClientFor(recipe, user=eric)
33 client.click(id=u'edit-build_daily')
34 client.waits.forElement(
35 classname=u'yui3-ichoicelist-content', timeout=FOR_ELEMENT)
36 client.click(link=u'Built daily')
37 client.waits.forElement(
38 jquery=u'("div#edit-build_daily a.editicon.sprite.edit")',
39 timeout=FOR_ELEMENT)
40 client.asserts.assertTextIn(
41 id=u'edit-build_daily', validator=u'Built daily')
42
43 transaction.commit()
44 freshly_fetched_recipe = Store.of(recipe).find(
45 SourcePackageRecipe, SourcePackageRecipe.id == recipe.id).one()
46 self.assertTrue(freshly_fetched_recipe.build_daily)
047
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-02-10 08:38:19 +0000
+++ lib/lp/testing/__init__.py 2011-02-14 07:50:33 +0000
@@ -158,7 +158,7 @@
158from lp.testing.fixture import ZopeEventHandlerFixture158from lp.testing.fixture import ZopeEventHandlerFixture
159from lp.testing.karma import KarmaRecorder159from lp.testing.karma import KarmaRecorder
160from lp.testing.matchers import Provides160from lp.testing.matchers import Provides
161from lp.testing.windmill import constants161from lp.testing.windmill import constants, lpuser
162162
163163
164class FakeTime:164class FakeTime:
@@ -772,6 +772,23 @@
772 # of things like https://launchpad.net/bugs/515494)772 # of things like https://launchpad.net/bugs/515494)
773 self.client.open(url=self.layer.appserver_root_url())773 self.client.open(url=self.layer.appserver_root_url())
774774
775 def getClientFor(self, obj, user=None, password='test', view_name=None):
776 """Return a new client, and the url that it has loaded."""
777 client = WindmillTestClient(self.suite_name)
778 if user is not None:
779 email = removeSecurityProxy(user).preferredemail.email
780 client.open(url=lpuser.get_basic_login_url(email, password))
781 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
782 if isinstance(obj, basestring):
783 url = obj
784 else:
785 url = canonical_url(
786 obj, view_name=view_name, force_local_path=True)
787 obj_url = self.layer.base_url + url
788 client.open(url=obj_url)
789 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
790 return client, obj_url
791
775792
776class YUIUnitTestCase(WindmillTestCase):793class YUIUnitTestCase(WindmillTestCase):
777794
778795
=== modified file 'lib/lp/testing/windmill/lpuser.py'
--- lib/lp/testing/windmill/lpuser.py 2011-02-10 04:00:00 +0000
+++ lib/lp/testing/windmill/lpuser.py 2011-02-14 07:50:33 +0000
@@ -11,6 +11,14 @@
11from lp.testing.windmill import constants11from lp.testing.windmill import constants
1212
1313
14def get_basic_login_url(email, password):
15 """Return the constructed url to login a user."""
16 base_url = windmill.settings['TEST_URL']
17 basic_auth_url = base_url.replace('http://', 'http://%s:%s@')
18 basic_auth_url = basic_auth_url + '+basiclogin'
19 return basic_auth_url % (email, password)
20
21
14class LaunchpadUser:22class LaunchpadUser:
15 """Object representing well-known user on Launchpad."""23 """Object representing well-known user on Launchpad."""
1624
@@ -32,10 +40,7 @@
3240
33 current_url = client.commands.execJS(41 current_url = client.commands.execJS(
34 code='windmill.testWin().location;')['result']['href']42 code='windmill.testWin().location;')['result']['href']
35 base_url = windmill.settings['TEST_URL']43 client.open(url=get_basic_login_url(self.email, self.password))
36 basic_auth_url = base_url.replace('http://', 'http://%s:%s@')
37 basic_auth_url = basic_auth_url + '+basiclogin'
38 client.open(url=basic_auth_url % (self.email, self.password))
39 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)44 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
40 client.open(url=current_url)45 client.open(url=current_url)
41 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)46 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
@@ -63,8 +68,7 @@
6368
64def login_person(person, email, password, client):69def login_person(person, email, password, client):
65 """Create a LaunchpadUser for a person and password."""70 """Create a LaunchpadUser for a person and password."""
66 user = LaunchpadUser(71 user = LaunchpadUser(person.displayname, email, password)
67 person.displayname, email, password)
68 user.ensure_login(client)72 user.ensure_login(client)
6973
7074