Merge lp:~wallyworld/launchpad/recipe-build-now into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 12441
Proposed branch: lp:~wallyworld/launchpad/recipe-build-now
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/request-build-popup
Diff against target: 1179 lines (+498/-134)
11 files modified
lib/canonical/launchpad/icing/icon-sprites.positioning (+56/-52)
lib/canonical/launchpad/icing/style-3-0.css.in (+8/-5)
lib/lp/code/browser/configure.zcml (+6/-0)
lib/lp/code/browser/sourcepackagerecipe.py (+140/-36)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+83/-0)
lib/lp/code/interfaces/sourcepackagerecipe.py (+4/-0)
lib/lp/code/javascript/requestbuild_overlay.js (+118/-27)
lib/lp/code/model/sourcepackagerecipe.py (+14/-0)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+36/-2)
lib/lp/code/windmill/tests/test_recipe_request_build.py (+26/-12)
lib/lp/testing/__init__.py (+7/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/recipe-build-now
Reviewer Review Type Date Requested Status
Tim Penhey (community) code Approve
Henning Eggers (community) ui Approve
Review via email: mp+49968@code.launchpad.net

Commit message

[r=thumper][ui=henninge][bug=687623,720474] Add a link/button on the recipe index page to allow a daily build to be manually initiated.

Description of the change

This change provides a way to allow users to manually request that a daily build of a recipe should be queued "now". This feature is only available if the recipe is configured to build daily and is marked as stale.

= =Implementation ==

A new view was written - SourcePackageRecipeRequestDailyBuildView
This view handles both the ajax and non-ajax cases. The request.is_ajax flag is used to see if it's an ajax call. If so, a different page template is used to just load the recipe builds and this info is returned to be inserted directly into the page without a refresh. For the non-ajax case, a page refresh is used to reload the entire recipe page and hence show the new pending builds.

In the case where there is already a build for one or more of the distroseries pending, no error is shown. Any new builds are simply displayed.

A new sprite was added - sourcepackage-recipe-build.png. This is used as the icon to display next to the ajax "Build now" link.

A drive by fix was done to the recipe vocabularies for archives and distroseries since they borked when the recipe index page was shown without a user logged in.

== Demo and QA ==

The following screenshots show how the link/button looks for the script and no script case

http://people.canonical.com/~ianb/request-daily-build.png
http://people.canonical.com/~ianb/request-daily-build-no-ajax.png

To QA, you just need to ensure there's an appropriate recipe available (stale, build daily etc) and click the "Build now" link. The link should then disappear and the Recent Builds table be updated.

== Tests ==

A Windmill test was written to test that the ajax enabled "Build now" link works.
Unit tests were written to check that the "Build now" feature only shows when relevant and to check the no script version.

bin/test -vvt test_sourcepackagerecipe
bin/test -vvt test_recipe_request_build

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Hi Ian,
thank you for this simple fix and taking the time to include some ajax and graceful degradation. It seems to work nicely but I do have a couple of issues I'd like to see addressed:

- I am not sure why the non-ajax case needs to include a button and does not get the nice new icon. Do we do that in other places already? The original idea was that ajax-enabled links are green, others blue and I think that still holds. This may be different here, though, because the link does not lead to a new page but simply starts an action on the same page. So the button may be OK if it is for that reason.
- The non-ajax case definitely needs some feed back. This is usually achieved by using the message boxes at the top of the page. "The build has been requested, see the Latest Builds table below." or something like that.
- The ajax case has a nice "requesting build" spinner when you click it but then just simply disappears. This leaves the user at a loss if the operation succeeded or not. (Can it fail btw?) A possible feedback might be to flash the Latest Builds table green, or rather the lines of the builds being started.
- The feed-back problem also made me think if the button is placed correctly in the first place. I actually think it should be placed by the table (which represents operational data about the builds) and not in the recipe information (which represents static data about the recipe). While writing this, I became very sure that it has to be moved. It should go right under the "Latest builds" heading in a line expressing the current state ("it is stale" but worded better ;).

On the whole this fix is a really nice start but I have to ask you to please fix the feed-back deficiencies before I can sign this off.

Cheers,
Henning

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

Hi Henning

Thanks for the detailed feedback. It's appreciated.

On 17/02/11 17:56, Henning Eggers wrote:
> Review: Needs Fixing ui
> Hi Ian,
> thank you for this simple fix and taking the time to include some ajax and graceful degradation. It seems to work nicely but I do have a couple of issues I'd like to see addressed:
>
> - I am not sure why the non-ajax case needs to include a button and does not get the nice new icon. Do we do that in other places already? The original idea was that ajax-enabled links are green, others blue and I think that still holds. This may be different here, though, because the link does not lead to a new page but simply starts an action on the same page. So the button may be OK if it is for that reason.

A form submission (post) is required for this operation in order to
start a write transaction. The issue is that without javascript a form
submission can be done with either a button or an image, but not a link.
In other similar places to this, we use a button (eg the vote or claim
review buttons on the mp page). Since buttons are used elsewhere for
this type of operation, I stuck with that convention. The alternative
was to go with an image that is done to look like a button or similar
but that's not done anywhere else. Because a post is required, a (blue)
link is not sufficient since that only does a get and starts a readonly
transaction.

> - The non-ajax case definitely needs some feed back. This is usually achieved by using the message boxes at the top of the page. "The build has been requested, see the Latest Builds table below." or something like that.

Good idea. Thanks. I'll do that.

> - The ajax case has a nice "requesting build" spinner when you click it but then just simply disappears. This leaves the user at a loss if the operation succeeded or not. (Can it fail btw?) A possible feedback might be to flash the Latest Builds table green, or rather the lines of the builds being started.

Any new builds rows are indeed flashed green using the standard yui
anim() call that we use in lp. Note that if any daily builds which
result from the Build Now click are already pending, they are not
reflashed. However this should be very rare,since the Build Now only
appears if the recipe is stale. Perhaps you missed seeing the row animation?

> - The feed-back problem also made me think if the button is placed correctly in the first place. I actually think it should be placed by the table (which represents operational data about the builds) and not in the recipe information (which represents static data about the recipe). While writing this, I became very sure that it has to be moved. It should go right under the "Latest builds" heading in a line expressing the current state ("it is stale" but worded better ;).
>

Ok. I'll mock something up and see how that looks.

> On the whole this fix is a really nice start but I have to ask you to please fix the feed-back deficiencies before I can sign this off.
>

No problem. The feedback was very good. Thanks.

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

>
> > - The feed-back problem also made me think if the button is placed correctly
> in the first place. I actually think it should be placed by the table (which
> represents operational data about the builds) and not in the recipe
> information (which represents static data about the recipe). While writing
> this, I became very sure that it has to be moved. It should go right under the
> "Latest builds" heading in a line expressing the current state ("it is stale"
> but worded better ;).
> >
>
> Ok. I'll mock something up and see how that looks.
>

A further thought on this. The Build Now link was put next to the recipe build_daily field on the ui because the two belong together. The Build Now link is only shown if the build_daily field is true. So it makes sense they are rendered together. The trouble with putting it in the builds table is that it decreases the visual association with the build_daily field which controls its existence. Also, using the words "is stale" or similar has a negative connotation and suggests to the user that action is required on their part to "correct" some problem. But there's no real problem as such and users may be enticed to keep selecting Build Now when the next scheduled daily build will take care of it. If the browser window is a largish size, then the new build rows will be visible when they are flashed green so that does give feedback that "something" has happended.

Revision history for this message
Henning Eggers (henninge) wrote :

Re: the non-ajax button

I had not made the connection to the form-submitting issue. In that case the button is appropriate, of course, as has been done elsewhere.

Re: Location of the link

I don't think the configurational relation of the daily build option and the availability of the action weighs heavier than the work flow for the user. Requesting an action must result in some visible change for the user but simply the option disappearing is not enough. That's why I think it should be moved to the table which is directly affected by the action. We cannot rely on a "largish size" of the browser window. On my laptop screen the top of the table is barely visible, even using chromium. Also, I just tried it a again and did not see a green flash at all but that might just me playing with the database to set the "stale" flag. ;-)

Re: "stale"

Sorry, I did not mean to put "stale" in the UI that's what I meant by "but worded better".

How about this (I am not sure if I got the wording right, though):

"<icon> There are no pending builds. _Build_now_"

After clicking _Build_now_:

"<icon> Build queued."
or
"<icon> Build pending"
or
"<icon> Next build in 10 hours" (if that is possible).

I could even go along with that being done in the old place but I am still not totally happy with mixing static and operational information. Also, it is probably doubling information from the Latest build table? My preference remains to place it on top of the table.

Also, I just noticed something else. Changing "Built daily" to "Built on request" (ajax popup) and back does not hide or show the new link. It takes a page reload to hide/show it. For the story to be complete, this should work in ajax, too.

Henning

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

Hi again

> RE: placement of Build Now button/link

I'll talk to Tim about the (valid) issues you raise and see what his
preferred option is etc.

>
> Also, I just noticed something else. Changing "Built daily" to "Built on request" (ajax popup) and back does not hide or show the new link. It takes a page reload to hide/show it. For the story to be complete, this should work in ajax, too.
>

Yes, this is a current known limitation and we are waiting on work by
Leonard on the javascript/webservices side before this issue can be
addressed. Good job on noticing it :-)

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

Henning,

This has all come from me, and Ian has provided what I asked for.

This is a specific action that only applies to daily builds, and as such, it should be next to the visual representation of the build schedule. This is where I want it.

We do have a "Request builds" action that is either in or next to the build table.

Tim

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

As discussed over mumble, there are a few things to fix.

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

Henning,

I've added a page notification for the html request build form and also the Build now button to give feedback since the new build rows cannot be flashed green.

http://people.canonical.com/~ianb/new-build-notification.png

I've kept the message simple: "x new recipe builds have been queued."
I experimented with attempting to list the new builds but in the end think simple is best.

Revision history for this message
Henning Eggers (henninge) wrote :

Tim,
ok, I understand the reasoning for the placement of the link and I am happy to go along with it now. What I cannot go along with, though, is that it simply disappears when the user clicks on it. I am using a larger screen today and so I see the green flash on the table but I am certain that users will be be missing it just like I did on my smaller laptop screen yesterday. Or they don't pay attention. Or they have tunnel vision. ;-) In any case, the action needs a feedback about success or failure in the same place where it is requested. So it should go: <icon> Build now -> <spinner> Requesting build ... -> <checkmark or info> 2 builds have been queued OR <error> Request failed. That response does not have to survive a page reload, though.

Ian,
the page notification looks good, thanks for adding that. ;)

Henning

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

> don't pay attention. Or they have tunnel vision. ;-) In any case, the action
> needs a feedback about success or failure in the same place where it is
> requested. So it should go: <icon> Build now -> <spinner> Requesting build ...
> -> <checkmark or info> 2 builds have been queued OR <error> Request failed.
> That response does not have to survive a page reload, though.

Hi Henning

I have added an informational message which appears when the ajax "Build now" action completes. The screenshot below shows how it looks:

http://people.canonical.com/~ianb/new-build-ajax-notification.png

This message replaces the "Requesting build..." spinner once the request completes and uses a timer so that after 20 seconds, the message disappears.

Hopefully this is sufficient :-)

Revision history for this message
Henning Eggers (henninge) wrote :

Looks great, thanks for adding this. ;-)

review: Approve (ui)
Revision history for this message
Tim Penhey (thumper) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/icing/icon-sprites'
0Binary files lib/canonical/launchpad/icing/icon-sprites 2010-06-11 18:39:35 +0000 and lib/canonical/launchpad/icing/icon-sprites 2011-02-23 10:25:23 +0000 differ0Binary files lib/canonical/launchpad/icing/icon-sprites 2010-06-11 18:39:35 +0000 and lib/canonical/launchpad/icing/icon-sprites 2011-02-23 10:25:23 +0000 differ
=== modified file 'lib/canonical/launchpad/icing/icon-sprites.positioning'
--- lib/canonical/launchpad/icing/icon-sprites.positioning 2010-10-31 20:18:45 +0000
+++ lib/canonical/launchpad/icing/icon-sprites.positioning 2011-02-23 10:25:23 +0000
@@ -3,7 +3,7 @@
3{3{
4 "../images/arrowLeft.png": [4 "../images/arrowLeft.png": [
5 0, 5 0,
6 -147546 -14918
7 ], 7 ],
8 "../images/cancel.png": [8 "../images/cancel.png": [
9 0, 9 0,
@@ -13,9 +13,9 @@
13 0, 13 0,
14 -344014 -3440
15 ], 15 ],
16 "../images/zoom-out.png": [16 "../images/build-needed.png": [
17 0, 17 0,
18 -1196618 -13442
19 ], 19 ],
20 "../images/team.png": [20 "../images/team.png": [
21 0, 21 0,
@@ -43,7 +43,7 @@
43 ], 43 ],
44 "../images/arrowTop.png": [44 "../images/arrowTop.png": [
45 0, 45 0,
46 -1442646 -14590
47 ], 47 ],
48 "../images/zoom-in.png": [48 "../images/zoom-in.png": [
49 0, 49 0,
@@ -55,23 +55,23 @@
55 ], 55 ],
56 "../images/blue-bar.png": [56 "../images/blue-bar.png": [
57 0, 57 0,
58 -1491858 -15082
59 ], 59 ],
60 "../images/arrowStart.png": [60 "../images/arrowStart.png": [
61 0, 61 0,
62 -1409862 -14262
63 ], 63 ],
64 "../images/ppa-icon-inactive.png": [64 "../images/ppa-icon-inactive.png": [
65 0, 65 0,
66 -1245866 -12458
67 ], 67 ],
68 "../images/build-needed.png": [68 "../images/zoom-out.png": [
69 0, 69 0,
70 -1327870 -11966
71 ], 71 ],
72 "../images/purple-bar.png": [72 "../images/purple-bar.png": [
73 0, 73 0,
74 -1524674 -15410
75 ], 75 ],
76 "../images/bullet.png": [76 "../images/bullet.png": [
77 0, 77 0,
@@ -79,11 +79,11 @@
79 ], 79 ],
80 "../images/info-large.png": [80 "../images/info-large.png": [
81 0, 81 0,
82 -1734082 -17504
83 ], 83 ],
84 "../images/trash-logo.png": [84 "../images/trash-logo.png": [
85 0, 85 0,
86 -2035886 -20340
87 ], 87 ],
88 "../images/warning.png": [88 "../images/warning.png": [
89 0, 89 0,
@@ -95,35 +95,35 @@
95 ], 95 ],
96 "../images/build-failure.png": [96 "../images/build-failure.png": [
97 0, 97 0,
98 -1344298 -13606
99 ], 99 ],
100 "../images/branch-large.png": [100 "../images/branch-large.png": [
101 0, 101 0,
102 -16066102 -16230
103 ], 103 ],
104 "../images/download-large.png": [104 "../images/download-large.png": [
105 0, 105 0,
106 -17158106 -17322
107 ], 107 ],
108 "../images/private-large.png": [108 "../images/private-large.png": [
109 0, 109 0,
110 -18250110 -18232
111 ], 111 ],
112 "../images/launchpad-large.png": [112 "../images/launchpad-large.png": [
113 0, 113 0,
114 -17522114 -17686
115 ], 115 ],
116 "../images/translation-file.png": [116 "../images/translation-file.png": [
117 0, 117 0,
118 -10818118 -10818
119 ], 119 ],
120 "../images/read-only.png": [120 "../images/source-package-recipe.png": [
121 0, 121 0,
122 -9998122 -12622
123 ], 123 ],
124 "../images/project-logo.png": [124 "../images/project-logo.png": [
125 0, 125 0,
126 -18860126 -18842
127 ], 127 ],
128 "../images/bug-medium.png": [128 "../images/bug-medium.png": [
129 0, 129 0,
@@ -135,7 +135,7 @@
135 ], 135 ],
136 "../images/tour-icon": [136 "../images/tour-icon": [
137 0, 137 0,
138 -15902138 -16066
139 ], 139 ],
140 "../images/trash-icon.png": [140 "../images/trash-icon.png": [
141 0, 141 0,
@@ -147,7 +147,7 @@
147 ], 147 ],
148 "../images/arrowBottom.png": [148 "../images/arrowBottom.png": [
149 0, 149 0,
150 -14590150 -14754
151 ], 151 ],
152 "../images/project.png": [152 "../images/project.png": [
153 0, 153 0,
@@ -173,17 +173,13 @@
173 0, 173 0,
174 -3768174 -3768
175 ], 175 ],
176 "../images/stop.png": [
177 0,
178 -11310
179 ],
180 "../images/person-logo.png": [176 "../images/person-logo.png": [
181 0, 177 0,
182 -19288178 -19270
183 ], 179 ],
184 "../images/distribution-logo.png": [180 "../images/distribution-logo.png": [
185 0, 181 0,
186 -18646182 -18628
187 ], 183 ],
188 "../images/retry.png": [184 "../images/retry.png": [
189 0, 185 0,
@@ -199,7 +195,7 @@
199 ], 195 ],
200 "../images/merge-proposal-icon.png": [196 "../images/merge-proposal-icon.png": [
201 0, 197 0,
202 -12786198 -12950
203 ], 199 ],
204 "../images/download.png": [200 "../images/download.png": [
205 0, 201 0,
@@ -207,7 +203,7 @@
207 ], 203 ],
208 "../images/arrowDown.png": [204 "../images/arrowDown.png": [
209 0, 205 0,
210 -13934206 -14098
211 ], 207 ],
212 "../images/package-binary.png": [208 "../images/package-binary.png": [
213 0, 209 0,
@@ -219,11 +215,11 @@
219 ], 215 ],
220 "../images/bug-status-expand.png": [216 "../images/bug-status-expand.png": [
221 0, 217 0,
222 -12622218 -12786
223 ], 219 ],
224 "../images/crowd-large.png": [220 "../images/crowd-large.png": [
225 0, 221 0,
226 -16430222 -16594
227 ], 223 ],
228 "../images/blueprint.png": [224 "../images/blueprint.png": [
229 0, 225 0,
@@ -245,9 +241,13 @@
245 0, 241 0,
246 -6228242 -6228
247 ], 243 ],
244 "../images/stop.png": [
245 0,
246 -11310
247 ],
248 "../images/flame-large.png": [248 "../images/flame-large.png": [
249 0, 249 0,
250 -16976250 -17140
251 ], 251 ],
252 "../images/bug-dupe-icon.png": [252 "../images/bug-dupe-icon.png": [
253 0, 253 0,
@@ -259,11 +259,11 @@
259 ], 259 ],
260 "../images/build-success.png": [260 "../images/build-success.png": [
261 0, 261 0,
262 -13114262 -13278
263 ], 263 ],
264 "../images/haspatch-icon.png": [264 "../images/haspatch-icon.png": [
265 0, 265 0,
266 -15738266 -15902
267 ], 267 ],
268 "../images/person-inactive-badge.png": [268 "../images/person-inactive-badge.png": [
269 0, 269 0,
@@ -279,7 +279,7 @@
279 ], 279 ],
280 "../images/team-logo.png": [280 "../images/team-logo.png": [
281 0, 281 0,
282 -19716282 -19698
283 ], 283 ],
284 "../images/arrowRight.png": [284 "../images/arrowRight.png": [
285 0, 285 0,
@@ -311,7 +311,7 @@
311 ], 311 ],
312 "../images/arrowUp.png": [312 "../images/arrowUp.png": [
313 0, 313 0,
314 -13770314 -13934
315 ], 315 ],
316 "../images/distribution.png": [316 "../images/distribution.png": [
317 0, 317 0,
@@ -319,11 +319,11 @@
319 ], 319 ],
320 "../images/error-large.png": [320 "../images/error-large.png": [
321 0, 321 0,
322 -16794322 -16958
323 ], 323 ],
324 "../images/news.png": [324 "../images/news.png": [
325 0, 325 0,
326 -15574326 -15738
327 ], 327 ],
328 "../images/treeExpanded.png": [328 "../images/treeExpanded.png": [
329 0, 329 0,
@@ -331,7 +331,7 @@
331 ], 331 ],
332 "../images/build-depwait.png": [332 "../images/build-depwait.png": [
333 0, 333 0,
334 -13606334 -13770
335 ], 335 ],
336 "../images/blueprint-essential.png": [336 "../images/blueprint-essential.png": [
337 0, 337 0,
@@ -351,7 +351,7 @@
351 ], 351 ],
352 "../images/product-logo.png": [352 "../images/product-logo.png": [
353 0, 353 0,
354 -19074354 -19056
355 ], 355 ],
356 "../images/blueprint-medium.png": [356 "../images/blueprint-medium.png": [
357 0, 357 0,
@@ -367,11 +367,11 @@
367 ], 367 ],
368 "../images/launchpad-logo.png": [368 "../images/launchpad-logo.png": [
369 0, 369 0,
370 -18432370 -18414
371 ], 371 ],
372 "../images/flame-logo.png": [372 "../images/flame-logo.png": [
373 0, 373 0,
374 -20144374 -20126
375 ], 375 ],
376 "../images/translation-template.png": [376 "../images/translation-template.png": [
377 0, 377 0,
@@ -383,7 +383,7 @@
383 ], 383 ],
384 "../images/meeting-logo.png": [384 "../images/meeting-logo.png": [
385 0, 385 0,
386 -19930386 -19912
387 ], 387 ],
388 "../images/treeCollapsed.png": [388 "../images/treeCollapsed.png": [
389 0, 389 0,
@@ -391,19 +391,19 @@
391 ], 391 ],
392 "../images/green-bar.png": [392 "../images/green-bar.png": [
393 0, 393 0,
394 -15082394 -15246
395 ], 395 ],
396 "../images/build-superseded.png": [396 "../images/build-superseded.png": [
397 0, 397 0,
398 -12950398 -13114
399 ], 399 ],
400 "../images/trash-large.png": [400 "../images/trash-large.png": [
401 0, 401 0,
402 -18068402 -18050
403 ], 403 ],
404 "../images/red-bar.png": [404 "../images/red-bar.png": [
405 0, 405 0,
406 -15410406 -15574
407 ], 407 ],
408 "../images/add.png": [408 "../images/add.png": [
409 0, 409 0,
@@ -413,9 +413,13 @@
413 0, 413 0,
414 -328414 -328
415 ], 415 ],
416 "../images/read-only.png": [
417 0,
418 -9998
419 ],
416 "../images/person-inactive-logo.png": [420 "../images/person-inactive-logo.png": [
417 0, 421 0,
418 -19502422 -19484
419 ], 423 ],
420 "../images/edit.png": [424 "../images/edit.png": [
421 0, 425 0,
@@ -427,11 +431,11 @@
427 ], 431 ],
428 "../images/warning-large.png": [432 "../images/warning-large.png": [
429 0, 433 0,
430 -16248434 -16412
431 ], 435 ],
432 "../images/arrowEnd.png": [436 "../images/arrowEnd.png": [
433 0, 437 0,
434 -14262438 -14426
435 ], 439 ],
436 "../images/cve.png": [440 "../images/cve.png": [
437 0, 441 0,
@@ -439,7 +443,7 @@
439 ], 443 ],
440 "../images/merge-proposal-large.png": [444 "../images/merge-proposal-large.png": [
441 0, 445 0,
442 -17886446 -17868
443 ], 447 ],
444 "../images/branch.png": [448 "../images/branch.png": [
445 0, 449 0,
@@ -469,4 +473,4 @@
469 0, 473 0,
470 -1148474 -1148
471 ]475 ]
472}476}
473\ No newline at end of file477\ No newline at end of file
474478
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-14 07:33:54 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-23 10:25:23 +0000
@@ -1471,10 +1471,10 @@
1471 Colors and fonts1471 Colors and fonts
1472*/1472*/
14731473
1474h1, h2, h3, 1474h1, h2, h3,
1475table.listing thead, 1475table.listing thead,
1476#homepage-stats strong, 1476#homepage-stats strong,
1477#application-footer strong, 1477#application-footer strong,
1478#application-summary strong {1478#application-summary strong {
1479 color: #5a5a5a;1479 color: #5a5a5a;
1480}1480}
@@ -2204,7 +2204,10 @@
2204 background-image: url(/@@/ppa-icon-inactive.png); /* sprite-ref: icon-sprites */2204 background-image: url(/@@/ppa-icon-inactive.png); /* sprite-ref: icon-sprites */
2205 background-repeat: no-repeat;2205 background-repeat: no-repeat;
2206 }2206 }
22072207.source-package-recipe {
2208 background-image: url(/@@/source-package-recipe.png); /* sprite-ref: icon-sprites */
2209 background-repeat: no-repeat;
2210 }
2208.bug-status-expand {2211.bug-status-expand {
2209 background-image: url(/@@/bug-status-expand.png); /* sprite-ref: icon-sprites */2212 background-image: url(/@@/bug-status-expand.png); /* sprite-ref: icon-sprites */
2210 background-repeat: no-repeat;2213 background-repeat: no-repeat;
22112214
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2011-02-10 04:16:09 +0000
+++ lib/lp/code/browser/configure.zcml 2011-02-23 10:25:23 +0000
@@ -1239,6 +1239,12 @@
1239 name="+builds"1239 name="+builds"
1240 template="../templates/sourcepackagerecipe-builds.pt"1240 template="../templates/sourcepackagerecipe-builds.pt"
1241 permission="launchpad.View"/>1241 permission="launchpad.View"/>
1242 <browser:page
1243 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
1244 layer="lp.code.publisher.CodeLayer"
1245 class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeRequestDailyBuildView"
1246 name="+request-daily-build"
1247 permission="launchpad.Edit"/>
1242 </facet>1248 </facet>
1243 <facet facet="branches">1249 <facet facet="branches">
1244 <browser:defaultView1250 <browser:defaultView
12451251
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-02-22 09:07:44 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000
@@ -180,12 +180,27 @@
180180
181 facet = 'branches'181 facet = 'branches'
182182
183 links = ('request_builds',)183 links = ('request_builds', 'request_daily_build',)
184184
185 def request_builds(self):185 def request_builds(self):
186 """Provide a link for requesting builds of a recipe."""186 """Provide a link for requesting builds of a recipe."""
187 return Link('+request-builds', 'Request build(s)', icon='add')187 return Link('+request-builds', 'Request build(s)', icon='add')
188188
189 def request_daily_build(self):
190 """Provide a link for requesting a daily build of a recipe."""
191 recipe = self.context
192 ppa = recipe.daily_build_archive
193 if (ppa is None or not recipe.build_daily or not recipe.is_stale
194 or not recipe.distroseries):
195 show_request_build = False
196 else:
197 has_upload = ppa.checkArchivePermission(recipe.owner)
198 show_request_build = has_upload
199
200 return Link(
201 '+request-daily-build', 'Build now',
202 enabled=show_request_build)
203
189204
190class SourcePackageRecipeView(LaunchpadView):205class SourcePackageRecipeView(LaunchpadView):
191 """Default view of a SourcePackageRecipe."""206 """Default view of a SourcePackageRecipe."""
@@ -292,6 +307,17 @@
292 return builds307 return builds
293308
294309
310def new_builds_notification_text(builds):
311 nr_builds = len(builds)
312 if not nr_builds:
313 builds_text = "All requested recipe builds are already queued."
314 elif nr_builds == 1:
315 builds_text = "1 new recipe build has been queued."
316 else:
317 builds_text = "%d new recipe builds have been queued." % nr_builds
318 return builds_text
319
320
295class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):321class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
296 """A view for requesting builds of a SourcePackageRecipe."""322 """A view for requesting builds of a SourcePackageRecipe."""
297323
@@ -341,14 +367,16 @@
341 other builds can ne queued and a message be displayed to the caller.367 other builds can ne queued and a message be displayed to the caller.
342 """368 """
343 errors = {}369 errors = {}
370 builds = []
344 for distroseries in data['distros']:371 for distroseries in data['distros']:
345 try:372 try:
346 self.context.requestBuild(373 build = self.context.requestBuild(
347 data['archive'], self.user, distroseries, manual=True)374 data['archive'], self.user, distroseries, manual=True)
375 builds.append(build)
348 except BuildAlreadyPending, e:376 except BuildAlreadyPending, e:
349 errors['distros'] = ("An identical build is already pending "377 errors['distros'] = ("An identical build is already pending "
350 "for %s." % e.distroseries)378 "for %s." % e.distroseries)
351 return errors379 return builds, errors
352380
353381
354class SourcePackageRecipeRequestBuildsHtmlView(382class SourcePackageRecipeRequestBuildsHtmlView(
@@ -367,44 +395,120 @@
367395
368 @action('Request builds', name='request')396 @action('Request builds', name='request')
369 def request_action(self, action, data):397 def request_action(self, action, data):
370 errors = self.requestBuild(data)398 builds, errors = self.requestBuild(data)
371 if errors:399 if errors:
372 [self.setFieldError(field, message)400 [self.setFieldError(field, message)
373 for (field, message) in errors.items()]401 for (field, message) in errors.items()]
374 return402 return
375 self.next_url = self.cancel_url403 self.next_url = self.cancel_url
376404 self.request.response.addNotification(
377405 new_builds_notification_text(builds))
378class SourcePackageRecipeRequestBuildsAjaxView(406
379 SourcePackageRecipeRequestBuildsView):407
380 """Supports AJAX form recipe build requests."""408class SourcePackageRecipeRequestBuildsAjaxView(
381409 SourcePackageRecipeRequestBuildsView):
382 def _process_error(self, data, errors, reason):410 """Supports AJAX form recipe build requests."""
383 """Set up the response and json data to return to the caller."""411
384 self.request.response.setStatus(400, reason)412 def _process_error(self, data, errors, reason):
385 self.request.response.setHeader('Content-type', 'application/json')413 """Set up the response and json data to return to the caller."""
386 return simplejson.dumps(errors)414 self.request.response.setStatus(400, reason)
387415 self.request.response.setHeader('Content-type', 'application/json')
388 def failure(self, action, data, errors):416 return simplejson.dumps(errors)
389 """Called by the form if validate() finds any errors.417
390418 def failure(self, action, data, errors):
391 We simply convert the errors to json and return that data to the419 """Called by the form if validate() finds any errors.
392 caller for display to the user.420
393 """421 We simply convert the errors to json and return that data to the
394 return self._process_error(data, self.widget_errors, "Validation")422 caller for display to the user.
395423 """
396 @action('Request builds', name='request', failure=failure)424 return self._process_error(data, self.widget_errors, "Validation")
397 def request_action(self, action, data):425
398 """User action for requesting a number of builds.426 @action('Request builds', name='request', failure=failure)
399427 def request_action(self, action, data):
400 The failure handler will handle any validation errors. We still need428 """User action for requesting a number of builds.
401 to handle errors which may occur when invoking the business logic.429
402 These "expected" errors are ones which result in a predefined message430 The failure handler will handle any validation errors. We still need
403 being displayed to the user. If the business method raises an431 to handle errors which may occur when invoking the business logic.
404 unexpected exception, that will be handled using the form's standard432 These "expected" errors are ones which result in a predefined message
405 exception processing mechanism (using response code 500).433 being displayed to the user. If the business method raises an
406 """434 unexpected exception, that will be handled using the form's standard
407 errors = self.requestBuild(data)435 exception processing mechanism (using response code 500).
436 """
437 builds, errors = self.requestBuild(data)
438 # If there are errors we return a json data snippet containing the
439 # errors instead of rendering the form. These errors are processed
440 # by the caller's response handler and displayed to the user.
441 if errors:
442 return self._process_error(data, errors, "Request Build")
443
444 @property
445 def builds(self):
446 return builds_for_recipe(self.context)
447
448
449class SourcePackageRecipeRequestDailyBuildView(LaunchpadFormView):
450 """Supports requests to perform a daily build for a recipe.
451
452 Renders the recipe builds table so that the recipe index page can be
453 updated with the new build records.
454
455 This view works for both ajax and html form requests.
456 """
457
458 # Attributes for the html version
459 page_title = "Build now"
460
461 class schema(Interface):
462 """Schema for requesting a build."""
463
464 @action('Build now', name='build')
465 def build_action(self, action, data):
466 recipe = self.context
467 builds = recipe.performDailyBuild()
468 if self.request.is_ajax:
469 template = ViewPageTemplateFile(
470 "../templates/sourcepackagerecipe-builds.pt")
471 return template(self)
472 else:
473 self.next_url = canonical_url(recipe)
474 self.request.response.addNotification(
475 new_builds_notification_text(builds))
476
477 @property
478 def builds(self):
479 return builds_for_recipe(self.context)
480
481
482class SourcePackageRecipeRequestBuildsAjaxView(
483 SourcePackageRecipeRequestBuildsView):
484 """Supports AJAX form recipe build requests."""
485
486 def _process_error(self, data, errors, reason):
487 """Set up the response and json data to return to the caller."""
488 self.request.response.setStatus(400, reason)
489 self.request.response.setHeader('Content-type', 'application/json')
490 return simplejson.dumps(errors)
491
492 def failure(self, action, data, errors):
493 """Called by the form if validate() finds any errors.
494
495 We simply convert the errors to json and return that data to the
496 caller for display to the user.
497 """
498 return self._process_error(data, self.widget_errors, "Validation")
499
500 @action('Request builds', name='request', failure=failure)
501 def request_action(self, action, data):
502 """User action for requesting a number of builds.
503
504 The failure handler will handle any validation errors. We still need
505 to handle errors which may occur when invoking the business logic.
506 These "expected" errors are ones which result in a predefined message
507 being displayed to the user. If the business method raises an
508 unexpected exception, that will be handled using the form's standard
509 exception processing mechanism (using response code 500).
510 """
511 builds, errors = self.requestBuild(data)
408 # If there are errors we return a json data snippet containing the512 # If there are errors we return a json data snippet containing the
409 # errors instead of rendering the form. These errors are processed513 # errors instead of rendering the form. These errors are processed
410 # by the caller's response handler and displayed to the user.514 # by the caller's response handler and displayed to the user.
411515
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-21 20:55:00 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-23 10:25:23 +0000
@@ -26,6 +26,7 @@
26from canonical.launchpad.testing.pages import (26from canonical.launchpad.testing.pages import (
27 extract_text,27 extract_text,
28 find_main_content,28 find_main_content,
29 find_tag_by_id,
29 find_tags_by_class,30 find_tags_by_class,
30 get_feedback_messages,31 get_feedback_messages,
31 get_radio_button_text_for_field,32 get_radio_button_text_for_field,
@@ -1227,6 +1228,88 @@
1227 self.assertEqual(1228 self.assertEqual(
1228 [build1, build2, build3, build4, build5], view.builds)1229 [build1, build2, build3, build4, build5], view.builds)
12291230
1231 def test_request_daily_builds_button_stale(self):
1232 # Recipes that are stale and are built daily have a build now link
1233 recipe = self.factory.makeSourcePackageRecipe(
1234 owner=self.chef, daily_build_archive=self.ppa,
1235 is_stale=True, build_daily=True)
1236 browser = self.getViewBrowser(recipe)
1237 build_button = find_tag_by_id(browser.contents, 'field.actions.build')
1238 self.assertIsNot(None, build_button)
1239
1240 def test_request_daily_builds_button_not_stale(self):
1241 # Recipes that are not stale do not have a build now link
1242 login(ANONYMOUS)
1243 recipe = self.factory.makeSourcePackageRecipe(
1244 owner=self.chef, daily_build_archive=self.ppa,
1245 is_stale=False, build_daily=True)
1246 browser = self.getViewBrowser(recipe)
1247 build_button = find_tag_by_id(browser.contents, 'field.actions.build')
1248 self.assertIs(None, build_button)
1249
1250 def test_request_daily_builds_button_not_daily(self):
1251 # Recipes that are not built daily do not have a build now link
1252 login(ANONYMOUS)
1253 recipe = self.factory.makeSourcePackageRecipe(
1254 owner=self.chef, daily_build_archive=self.ppa,
1255 is_stale=True, build_daily=False)
1256 browser = self.getViewBrowser(recipe)
1257 build_button = find_tag_by_id(browser.contents, 'field.actions.build')
1258 self.assertIs(None, build_button)
1259
1260 def test_request_daily_builds_button_no_daily_ppa(self):
1261 # Recipes that have no daily build ppa do not have a build now link
1262 login(ANONYMOUS)
1263 recipe = self.factory.makeSourcePackageRecipe(
1264 owner=self.chef, is_stale=True, build_daily=True)
1265 naked_recipe = removeSecurityProxy(recipe)
1266 naked_recipe.daily_build_archive = None
1267 browser = self.getViewBrowser(recipe)
1268 build_button = find_tag_by_id(browser.contents, 'field.actions.build')
1269 self.assertIs(None, build_button)
1270
1271 def test_request_daily_builds_button_ppa_with_no_permissions(self):
1272 # Recipes that have a daily build ppa without upload permissions
1273 # do not have a build now link
1274 login(ANONYMOUS)
1275 distroseries = self.factory.makeSourcePackageRecipeDistroseries()
1276 person = self.factory.makePerson()
1277 daily_build_archive = self.factory.makeArchive(
1278 distribution=distroseries.distribution, owner=person)
1279 recipe = self.factory.makeSourcePackageRecipe(
1280 owner=self.chef, daily_build_archive=daily_build_archive,
1281 is_stale=True, build_daily=True)
1282 browser = self.getViewBrowser(recipe)
1283 build_button = find_tag_by_id(browser.contents, 'field.actions.build')
1284 self.assertIs(None, build_button)
1285
1286 def test_request_daily_builds_ajax_link_not_rendered(self):
1287 # The Build now link should not be rendered without javascript.
1288 recipe = self.factory.makeSourcePackageRecipe(
1289 owner=self.chef, daily_build_archive=self.ppa,
1290 is_stale=True, build_daily=True)
1291 browser = self.getViewBrowser(recipe)
1292 build_link = find_tag_by_id(browser.contents, 'request-daily-builds')
1293 self.assertIs(None, build_link)
1294
1295 def test_request_daily_builds_action(self):
1296 # Daily builds should be triggered when requested.
1297 recipe = self.factory.makeSourcePackageRecipe(
1298 owner=self.chef, daily_build_archive=self.ppa,
1299 is_stale=True, build_daily=True)
1300 browser = self.getViewBrowser(recipe)
1301 browser.getControl('Build now').click()
1302 login(ANONYMOUS)
1303 builds = recipe.getPendingBuilds()
1304 build_distros = [
1305 build.distroseries.displayname for build in builds]
1306 build_distros.sort()
1307 # Our recipe has a Warty distroseries
1308 self.assertEqual(['Warty'], build_distros)
1309 self.assertEqual(
1310 set([2505]),
1311 set(build.buildqueue_record.lastscore for build in builds))
1312
1230 def test_request_builds_page(self):1313 def test_request_builds_page(self):
1231 """Ensure the +request-builds page is sane."""1314 """Ensure the +request-builds page is sane."""
1232 recipe = self.makeRecipe()1315 recipe = self.makeRecipe()
12331316
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-18 03:39:25 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000
@@ -136,6 +136,10 @@
136 able to upload to the archive.136 able to upload to the archive.
137 """137 """
138138
139 @export_write_operation()
140 def performDailyBuild():
141 """Perform a build into the daily build archive."""
142
139143
140class ISourcePackageRecipeEdit(Interface):144class ISourcePackageRecipeEdit(Interface):
141 """ISourcePackageRecipe methods that require launchpad.Edit permission."""145 """ISourcePackageRecipe methods that require launchpad.Edit permission."""
142146
=== modified file 'lib/lp/code/javascript/requestbuild_overlay.js'
--- lib/lp/code/javascript/requestbuild_overlay.js 2011-02-18 01:06:19 +0000
+++ lib/lp/code/javascript/requestbuild_overlay.js 2011-02-23 10:25:23 +0000
@@ -12,7 +12,8 @@
1212
13var lp_client;13var lp_client;
14var request_build_overlay = null;14var request_build_overlay = null;
15var response_handler;15var request_build_response_handler;
16var request_daily_build_response_handler;
1617
17function set_up_lp_client() {18function set_up_lp_client() {
18 if (lp_client === undefined) {19 if (lp_client === undefined) {
@@ -55,7 +56,7 @@
55 }56 }
56};57};
5758
58namespace.connect_requestbuild = function() {59namespace.connect_requestbuilds = function() {
5960
60 var request_build_handle = Y.one('#request-builds');61 var request_build_handle = Y.one('#request-builds');
61 request_build_handle.addClass('js-action');62 request_build_handle.addClass('js-action');
@@ -80,26 +81,102 @@
80 request_build_overlay.render();81 request_build_overlay.render();
81 }82 }
82 request_build_overlay.clearError();83 request_build_overlay.clearError();
83 var temp_spinner = [84 var loading_spinner = [
84 '<div id="temp-spinner">',85 '<div id="temp-spinner">',
85 '<img src="/@@/spinner"/>Loading...',86 '<img src="/@@/spinner"/>Loading...',
86 '</div>'].join('');87 '</div>'].join('');
87 request_build_overlay.form_node.set("innerHTML", temp_spinner);88 request_build_overlay.form_node.set("innerHTML", loading_spinner);
88 request_build_overlay.loadFormContentAndRender('+builds/++form++');89 request_build_overlay.loadFormContentAndRender('+builds/++form++');
89 request_build_overlay.show();90 request_build_overlay.show();
90 });91 });
9192
92 // Wire up the processing hooks93 // Wire up the processing hooks
93 response_handler = new RequestResponseHandler();94 request_build_response_handler = new RequestResponseHandler();
94 response_handler.clearProgressUI = function() {95 request_build_response_handler.clearProgressUI = function() {
95 destroy_temporary_spinner();96 destroy_temporary_spinner();
96 };97 };
97 response_handler.showError = function(error) {98 request_build_response_handler.showError = function(error) {
98 request_build_overlay.showError(error);99 request_build_overlay.showError(error);
99 Y.log(error);100 Y.log(error);
100 };101 };
101};102};
102103
104var NO_BUILDS_MESSAGE = "All requested recipe builds are already queued.";
105var ONE_BUILD_MESSAGE = "1 new recipe build has been queued.";
106var MANY_BUILDS_MESSAGE = "{nr_new} new recipe builds have been queued.";
107
108namespace.connect_requestdailybuild = function() {
109
110 var request_daily_build_handle = Y.one('#request-daily-build');
111 request_daily_build_handle.on('click', function(e) {
112 e.preventDefault();
113
114 create_temporary_spinner(
115 "Requesting build...", request_daily_build_handle);
116 request_daily_build_handle.addClass("unseen");
117
118 var base_url = LP.client.cache.context.web_link;
119 var submit_url = base_url+"/+request-daily-build";
120 var current_builds = harvest_current_build_records();
121
122 var qs = LP.client.append_qs('', 'field.actions.build', 'Build now');
123 var y_config = {
124 method: "POST",
125 headers: {'Accept': 'application/xhtml'},
126 on: {
127 failure: request_daily_build_response_handler.getErrorHandler(
128 function(handler, id, response) {
129 request_daily_build_handle.removeClass("unseen");
130 var server_error = 'Server error, ' +
131 'please contact an administrator.';
132 handler.showError(server_error);
133 }),
134 success:
135 request_daily_build_response_handler.getSuccessHandler(
136 function(handler, id, response) {
137 var nr_new = display_build_records(
138 response.responseText, current_builds);
139 var new_builds_message;
140 switch (nr_new) {
141 case 0:
142 new_builds_message = NO_BUILDS_MESSAGE;
143 break;
144 case 1:
145 new_builds_message = ONE_BUILD_MESSAGE;
146 break;
147 default:
148 new_builds_message =
149 Y.Lang.substitute(
150 MANY_BUILDS_MESSAGE,
151 {nr_new: nr_new});
152 }
153 var build_message_node = Y.Node.create([
154 '<div id="new-builds-info" class="build-informational">',
155 new_builds_message,
156 '</div>'].join(''));
157 request_daily_build_handle.insert(
158 build_message_node,
159 request_daily_build_handle);
160 Y.later(20000, build_message_node, 'hide', true);
161 }
162 )
163 },
164 data: qs
165 };
166 Y.io(submit_url, y_config);
167 });
168
169 // Wire up the processing hooks
170 request_daily_build_response_handler = new RequestResponseHandler();
171 request_daily_build_response_handler.clearProgressUI = function() {
172 destroy_temporary_spinner();
173 };
174 request_daily_build_response_handler.showError = function(error) {
175 alert(error);
176 Y.log(error);
177 };
178};
179
103/*180/*
104 * A function to return the current build records as displayed on the page181 * A function to return the current build records as displayed on the page
105 */182 */
@@ -118,14 +195,34 @@
118}195}
119196
120/*197/*
198 * Render build records and flash the new ones
199 */
200function display_build_records(build_records_markup, current_builds) {
201 var target = Y.one('#builds-target');
202 target.set('innerHTML', build_records_markup);
203 var new_builds = harvest_current_build_records();
204 var nr_new_builds = 0;
205 Y.Array.each(new_builds, function(row_id) {
206 if( current_builds.indexOf(row_id)>=0 )
207 return;
208 nr_new_builds += 1;
209 var row = Y.one('#'+row_id);
210 var anim = Y.lazr.anim.green_flash({node: row});
211 anim.run();
212 });
213 return nr_new_builds;
214}
215
216/*
121 * Perform any client side validation217 * Perform any client side validation
122 * Return: true if data is valid218 * Return: true if data is valid
123 */219 */
124function validate(data) {220function validate(data) {
125 var distros = data['field.distros']221 var distros = data['field.distros']
126 if (Y.Object.size(distros) == 0) {222 if (Y.Object.size(distros) == 0) {
127 response_handler.showError("You need to specify at least one " +223 request_build_response_handler.showError(
128 "distro series for which to build.");224 "You need to specify at least one distro series for " +
225 "which to build.");
129 return false;226 return false;
130 }227 }
131 return true;228 return true;
@@ -137,7 +234,9 @@
137function do_request_builds(data) {234function do_request_builds(data) {
138 if (!validate(data))235 if (!validate(data))
139 return;236 return;
140 create_temporary_spinner();237 var spinner_location = Y.one('.yui3-lazr-formoverlay-actions');
238 create_temporary_spinner("Requesting builds...", spinner_location);
239
141 var base_url = LP.client.cache.context.web_link;240 var base_url = LP.client.cache.context.web_link;
142 var submit_url = base_url+"/+builds";241 var submit_url = base_url+"/+builds";
143 var current_builds = harvest_current_build_records();242 var current_builds = harvest_current_build_records();
@@ -145,14 +244,14 @@
145 method: "POST",244 method: "POST",
146 headers: {'Accept': 'application/json; application/xhtml'},245 headers: {'Accept': 'application/json; application/xhtml'},
147 on: {246 on: {
148 failure: response_handler.getErrorHandler(247 failure: request_build_response_handler.getErrorHandler(
149 function(handler, id, response) {248 function(handler, id, response) {
150 if( response.status >= 500 ) {249 if( response.status >= 500 ) {
151 // There's some error content we need to display.250 // There's some error content we need to display.
152 request_build_overlay.set(251 request_build_overlay.set(
153 'form_content', response.responseText);252 'form_content', response.responseText);
154 request_build_overlay.get("form_submit_button")253 request_build_overlay.get("form_submit_button")
155 .setStyle('display', 'none');254 .addClass('unseen');
156 request_build_overlay.renderUI();255 request_build_overlay.renderUI();
157 //We want to force the form to be re-created256 //We want to force the form to be re-created
158 request_build_overlay = null;257 request_build_overlay = null;
@@ -164,19 +263,11 @@
164 errors.push(error_info[field_name]);263 errors.push(error_info[field_name]);
165 handler.showError(errors);264 handler.showError(errors);
166 }),265 }),
167 success: response_handler.getSuccessHandler(266 success: request_build_response_handler.getSuccessHandler(
168 function(handler, id, response) {267 function(handler, id, response) {
169 request_build_overlay.hide();268 request_build_overlay.hide();
170 var target = Y.one('#builds-target');269 display_build_records(
171 target.set('innerHTML', response.responseText);270 response.responseText, current_builds)
172 var new_builds = harvest_current_build_records();
173 Y.Array.each(new_builds, function(row_id) {
174 if (current_builds.indexOf(row_id)>=0)
175 return;
176 var row = Y.one('#'+row_id);
177 var anim = Y.lazr.anim.green_flash({node: row});
178 anim.run();
179 });
180 })271 })
181 },272 },
182 form: {273 form: {
@@ -190,14 +281,14 @@
190/*281/*
191 * Show the temporary "Requesting..." text282 * Show the temporary "Requesting..." text
192 */283 */
193function create_temporary_spinner() {284function create_temporary_spinner(text, node) {
194 // Add the temp "Requesting build..." text285 // Add the temp "Requesting build..." text
195 var temp_spinner = Y.Node.create([286 var temp_spinner = Y.Node.create([
196 '<div id="temp-spinner">',287 '<div id="temp-spinner">',
197 '<img src="/@@/spinner"/>Requesting build...',288 '<img src="/@@/spinner"/>',
289 text,
198 '</div>'].join(''));290 '</div>'].join(''));
199 var request_build_handle = Y.one('.yui3-lazr-formoverlay-actions');291 node.insert(temp_spinner, node);
200 request_build_handle.insert(temp_spinner, request_build_handle);
201}292}
202293
203/*294/*
204295
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2011-02-18 23:51:57 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000
@@ -281,6 +281,20 @@
281 queue_record.manualScore(queue_record.lastscore + 100)281 queue_record.manualScore(queue_record.lastscore + 100)
282 return build282 return build
283283
284 def performDailyBuild(self):
285 """See `ISourcePackageRecipe`."""
286 builds = []
287 self.is_stale = False
288 for distroseries in self.distroseries:
289 try:
290 build = self.requestBuild(
291 self.daily_build_archive, self.owner,
292 distroseries, PackagePublishingPocket.RELEASE)
293 builds.append(build)
294 except BuildAlreadyPending:
295 continue
296 return builds
297
284 def getBuilds(self):298 def getBuilds(self):
285 """See `ISourcePackageRecipe`."""299 """See `ISourcePackageRecipe`."""
286 where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD300 where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD
287301
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-02-22 09:07:44 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-02-23 10:25:23 +0000
@@ -16,6 +16,18 @@
16 font-family: "UbuntuBeta Mono","Ubuntu Mono",monospace;16 font-family: "UbuntuBeta Mono","Ubuntu Mono",monospace;
17 margin-top: -15px;17 margin-top: -15px;
18 }18 }
19 .build-informational {
20 background: #d4e8ff url('/+icing/blue-fade-to-grey');
21 border: solid #666;
22 border-width: 1px 2px 2px 1px;
23 color: black;
24 padding: 5px 5px 5px 5px;
25 margin-right: 40px
26 }
27 .build-informational::before {
28 padding-right: 5px;
29 content: url('/@@/info');
30 }
19 </style>31 </style>
20</metal:block>32</metal:block>
2133
@@ -60,6 +72,23 @@
60 </a>72 </a>
61 </dt>73 </dt>
62 <dd tal:content="structure view/daily_build_widget"/>74 <dd tal:content="structure view/daily_build_widget"/>
75 <dd
76 tal:define="link context/menu:context/request_daily_build"
77 tal:condition="link/enabled"
78 >
79 <noscript>
80 <form action="+request-daily-build"
81 method="post"
82 id="request-daily-build-form">
83 <input type="submit" name="field.actions.build"
84 id="field.actions.build" value="Build now" />
85 </form>
86 </noscript>
87 <a id="request-daily-build"
88 class="sprite source-package-recipe js-action unseen"
89 tal:attributes="href link/url"
90 tal:content="link/text"/>
91 </dd>
63 </dl>92 </dl>
6493
65 <dl id="owner">94 <dl id="owner">
@@ -116,11 +145,16 @@
116 if(Y.UA.ie) {145 if(Y.UA.ie) {
117 return;146 return;
118 }147 }
119
120 Y.on('load', function() {148 Y.on('load', function() {
121 var logged_in = LP.client.links['me'] !== undefined;149 var logged_in = LP.client.links['me'] !== undefined;
122 if (logged_in) {150 if (logged_in) {
123 Y.lp.code.requestbuild_overlay.connect_requestbuild();151 build_now_link = Y.one('#request-daily-build');
152 if( build_now_link != null ) {
153 build_now_link.removeClass('unseen');
154 Y.lp.code.requestbuild_overlay.connect_requestdailybuild();
155 }
156 Y.lp.code.requestbuild_overlay.connect_requestbuilds();
157
124 }158 }
125 }, window);159 }, window);
126 });"160 });"
127161
=== modified file 'lib/lp/code/windmill/tests/test_recipe_request_build.py'
--- lib/lp/code/windmill/tests/test_recipe_request_build.py 2011-02-18 01:06:19 +0000
+++ lib/lp/code/windmill/tests/test_recipe_request_build.py 2011-02-23 10:25:23 +0000
@@ -13,13 +13,12 @@
13from lp.testing.windmill.constants import (13from lp.testing.windmill.constants import (
14 FOR_ELEMENT,14 FOR_ELEMENT,
15 PAGE_LOAD,15 PAGE_LOAD,
16 SLEEP,
17 )16 )
18from lp.testing.windmill.lpuser import login_person17from lp.testing.windmill.lpuser import login_person
19from lp.app.browser.tales import PPAFormatterAPI18from lp.app.browser.tales import PPAFormatterAPI
20from lp.code.windmill.testing import CodeWindmillLayer19from lp.code.windmill.testing import CodeWindmillLayer
21from lp.soyuz.model.processor import ProcessorFamily20from lp.soyuz.model.processor import ProcessorFamily
22from lp.testing import WindmillTestCase21from lp.testing import WindmillTestCase, quote_jquery_expression
2322
2423
25class TestRecipeBuild(WindmillTestCase):24class TestRecipeBuild(WindmillTestCase):
@@ -50,7 +49,7 @@
50 owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',49 owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',
51 description=u'This recipe builds a foo for disto bar, with my'50 description=u'This recipe builds a foo for disto bar, with my'
52 ' Secret Squirrel changes.', branches=[cake_branch],51 ' Secret Squirrel changes.', branches=[cake_branch],
53 daily_build_archive=self.ppa)52 daily_build_archive=self.ppa, build_daily=True, is_stale=True)
54 transaction.commit()53 transaction.commit()
55 login_person(self.chef, "chef@example.com", "test", self.client)54 login_person(self.chef, "chef@example.com", "test", self.client)
5655
@@ -74,8 +73,8 @@
7473
75 # Ensure it shows up.74 # Ensure it shows up.
76 client.waits.forElement(75 client.waits.forElement(
77 xpath = (u'//tr[contains(@class, "package-build")]/td[4]'76 jquery=u"('tr.package-build a[href$=\"%s\"]')"
78 '/a[@href="%s"]') % PPAFormatterAPI(self.ppa).url(),77 % quote_jquery_expression(PPAFormatterAPI(self.ppa).url()),
79 timeout=FOR_ELEMENT)78 timeout=FOR_ELEMENT)
8079
81 # And try the same one again.80 # And try the same one again.
@@ -85,12 +84,27 @@
8584
86 # And check that there's an error.85 # And check that there's an error.
87 client.waits.forElement(86 client.waits.forElement(
88 xpath = (87 jquery=u"('div.yui3-lazr-formoverlay-errors ul li')",
89 u'//div[contains(@class, "yui3-lazr-formoverlay-errors")]'88 timeout=FOR_ELEMENT)
90 '/ul/li'), timeout=FOR_ELEMENT)89
91 client.asserts.assertText(90 client.asserts.assertTextIn(
92 xpath = (91 jquery=u"('div.yui3-lazr-formoverlay-errors ul li')[0]",
93 u'//div[contains(@class, "yui3-lazr-formoverlay-errors")]'
94 '/ul/li'),
95 validator=u'An identical build is already pending for %s %s.'92 validator=u'An identical build is already pending for %s %s.'
96 % (self.ppa.distribution.name, self.squirrel.name))93 % (self.ppa.distribution.name, self.squirrel.name))
94
95 def test_recipe_daily_build_request(self):
96 """Request a recipe build."""
97
98 client = self.client
99 client.open(url=canonical_url(self.recipe))
100 client.waits.forElement(
101 id=u'request-daily-build', timeout=PAGE_LOAD)
102
103 # Request a daily build.
104 client.click(id=u'request-daily-build')
105
106 # Ensure it shows up.
107 client.waits.forElement(
108 jquery=u"('tr.package-build a[href$=\"%s\"]')"
109 % quote_jquery_expression(PPAFormatterAPI(self.ppa).url()),
110 timeout=FOR_ELEMENT)
97111
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-02-11 00:25:51 +0000
+++ lib/lp/testing/__init__.py 2011-02-23 10:25:23 +0000
@@ -28,6 +28,7 @@
28 'normalize_whitespace',28 'normalize_whitespace',
29 'oauth_access_token_for',29 'oauth_access_token_for',
30 'person_logged_in',30 'person_logged_in',
31 'quote_jquery_expression'
31 'record_statements',32 'record_statements',
32 'run_with_login',33 'run_with_login',
33 'run_with_storm_debug',34 'run_with_storm_debug',
@@ -790,6 +791,12 @@
790 return client, obj_url791 return client, obj_url
791792
792793
794def quote_jquery_expression(expression):
795 """jquery requires meta chars used in literals escaped with \\"""
796 return re.sub(
797 "([#!$%&()+,./:;?@~|^{}\\[\\]`*\\\'\\\"])", r"\\\\\1", expression)
798
799
793class YUIUnitTestCase(WindmillTestCase):800class YUIUnitTestCase(WindmillTestCase):
794801
795 layer = None802 layer = None