Merge lp:~wallyworld/launchpad/recipe-build-now into lp:launchpad
- recipe-build-now
- Merge into devel
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 | ||||||||
Related bugs: |
|
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]
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 - SourcePackageRe
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-
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://
http://
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_sourcepack
bin/test -vvt test_recipe_
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.
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.
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
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/
addressed. Good job on noticing it :-)
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
Tim Penhey (thumper) wrote : | # |
As discussed over mumble, there are a few things to fix.
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://
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.
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
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://
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 :-)
Henning Eggers (henninge) wrote : | # |
Looks great, thanks for adding this. ;-)
Tim Penhey (thumper) : | # |
Preview Diff
1 | === modified file 'lib/canonical/launchpad/icing/icon-sprites' | |||
2 | 0 | Binary 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 | 0 | Binary 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 |
3 | === modified file 'lib/canonical/launchpad/icing/icon-sprites.positioning' | |||
4 | --- lib/canonical/launchpad/icing/icon-sprites.positioning 2010-10-31 20:18:45 +0000 | |||
5 | +++ lib/canonical/launchpad/icing/icon-sprites.positioning 2011-02-23 10:25:23 +0000 | |||
6 | @@ -3,7 +3,7 @@ | |||
7 | 3 | { | 3 | { |
8 | 4 | "../images/arrowLeft.png": [ | 4 | "../images/arrowLeft.png": [ |
9 | 5 | 0, | 5 | 0, |
11 | 6 | -14754 | 6 | -14918 |
12 | 7 | ], | 7 | ], |
13 | 8 | "../images/cancel.png": [ | 8 | "../images/cancel.png": [ |
14 | 9 | 0, | 9 | 0, |
15 | @@ -13,9 +13,9 @@ | |||
16 | 13 | 0, | 13 | 0, |
17 | 14 | -3440 | 14 | -3440 |
18 | 15 | ], | 15 | ], |
20 | 16 | "../images/zoom-out.png": [ | 16 | "../images/build-needed.png": [ |
21 | 17 | 0, | 17 | 0, |
23 | 18 | -11966 | 18 | -13442 |
24 | 19 | ], | 19 | ], |
25 | 20 | "../images/team.png": [ | 20 | "../images/team.png": [ |
26 | 21 | 0, | 21 | 0, |
27 | @@ -43,7 +43,7 @@ | |||
28 | 43 | ], | 43 | ], |
29 | 44 | "../images/arrowTop.png": [ | 44 | "../images/arrowTop.png": [ |
30 | 45 | 0, | 45 | 0, |
32 | 46 | -14426 | 46 | -14590 |
33 | 47 | ], | 47 | ], |
34 | 48 | "../images/zoom-in.png": [ | 48 | "../images/zoom-in.png": [ |
35 | 49 | 0, | 49 | 0, |
36 | @@ -55,23 +55,23 @@ | |||
37 | 55 | ], | 55 | ], |
38 | 56 | "../images/blue-bar.png": [ | 56 | "../images/blue-bar.png": [ |
39 | 57 | 0, | 57 | 0, |
41 | 58 | -14918 | 58 | -15082 |
42 | 59 | ], | 59 | ], |
43 | 60 | "../images/arrowStart.png": [ | 60 | "../images/arrowStart.png": [ |
44 | 61 | 0, | 61 | 0, |
46 | 62 | -14098 | 62 | -14262 |
47 | 63 | ], | 63 | ], |
48 | 64 | "../images/ppa-icon-inactive.png": [ | 64 | "../images/ppa-icon-inactive.png": [ |
49 | 65 | 0, | 65 | 0, |
50 | 66 | -12458 | 66 | -12458 |
51 | 67 | ], | 67 | ], |
53 | 68 | "../images/build-needed.png": [ | 68 | "../images/zoom-out.png": [ |
54 | 69 | 0, | 69 | 0, |
56 | 70 | -13278 | 70 | -11966 |
57 | 71 | ], | 71 | ], |
58 | 72 | "../images/purple-bar.png": [ | 72 | "../images/purple-bar.png": [ |
59 | 73 | 0, | 73 | 0, |
61 | 74 | -15246 | 74 | -15410 |
62 | 75 | ], | 75 | ], |
63 | 76 | "../images/bullet.png": [ | 76 | "../images/bullet.png": [ |
64 | 77 | 0, | 77 | 0, |
65 | @@ -79,11 +79,11 @@ | |||
66 | 79 | ], | 79 | ], |
67 | 80 | "../images/info-large.png": [ | 80 | "../images/info-large.png": [ |
68 | 81 | 0, | 81 | 0, |
70 | 82 | -17340 | 82 | -17504 |
71 | 83 | ], | 83 | ], |
72 | 84 | "../images/trash-logo.png": [ | 84 | "../images/trash-logo.png": [ |
73 | 85 | 0, | 85 | 0, |
75 | 86 | -20358 | 86 | -20340 |
76 | 87 | ], | 87 | ], |
77 | 88 | "../images/warning.png": [ | 88 | "../images/warning.png": [ |
78 | 89 | 0, | 89 | 0, |
79 | @@ -95,35 +95,35 @@ | |||
80 | 95 | ], | 95 | ], |
81 | 96 | "../images/build-failure.png": [ | 96 | "../images/build-failure.png": [ |
82 | 97 | 0, | 97 | 0, |
84 | 98 | -13442 | 98 | -13606 |
85 | 99 | ], | 99 | ], |
86 | 100 | "../images/branch-large.png": [ | 100 | "../images/branch-large.png": [ |
87 | 101 | 0, | 101 | 0, |
89 | 102 | -16066 | 102 | -16230 |
90 | 103 | ], | 103 | ], |
91 | 104 | "../images/download-large.png": [ | 104 | "../images/download-large.png": [ |
92 | 105 | 0, | 105 | 0, |
94 | 106 | -17158 | 106 | -17322 |
95 | 107 | ], | 107 | ], |
96 | 108 | "../images/private-large.png": [ | 108 | "../images/private-large.png": [ |
97 | 109 | 0, | 109 | 0, |
99 | 110 | -18250 | 110 | -18232 |
100 | 111 | ], | 111 | ], |
101 | 112 | "../images/launchpad-large.png": [ | 112 | "../images/launchpad-large.png": [ |
102 | 113 | 0, | 113 | 0, |
104 | 114 | -17522 | 114 | -17686 |
105 | 115 | ], | 115 | ], |
106 | 116 | "../images/translation-file.png": [ | 116 | "../images/translation-file.png": [ |
107 | 117 | 0, | 117 | 0, |
108 | 118 | -10818 | 118 | -10818 |
109 | 119 | ], | 119 | ], |
111 | 120 | "../images/read-only.png": [ | 120 | "../images/source-package-recipe.png": [ |
112 | 121 | 0, | 121 | 0, |
114 | 122 | -9998 | 122 | -12622 |
115 | 123 | ], | 123 | ], |
116 | 124 | "../images/project-logo.png": [ | 124 | "../images/project-logo.png": [ |
117 | 125 | 0, | 125 | 0, |
119 | 126 | -18860 | 126 | -18842 |
120 | 127 | ], | 127 | ], |
121 | 128 | "../images/bug-medium.png": [ | 128 | "../images/bug-medium.png": [ |
122 | 129 | 0, | 129 | 0, |
123 | @@ -135,7 +135,7 @@ | |||
124 | 135 | ], | 135 | ], |
125 | 136 | "../images/tour-icon": [ | 136 | "../images/tour-icon": [ |
126 | 137 | 0, | 137 | 0, |
128 | 138 | -15902 | 138 | -16066 |
129 | 139 | ], | 139 | ], |
130 | 140 | "../images/trash-icon.png": [ | 140 | "../images/trash-icon.png": [ |
131 | 141 | 0, | 141 | 0, |
132 | @@ -147,7 +147,7 @@ | |||
133 | 147 | ], | 147 | ], |
134 | 148 | "../images/arrowBottom.png": [ | 148 | "../images/arrowBottom.png": [ |
135 | 149 | 0, | 149 | 0, |
137 | 150 | -14590 | 150 | -14754 |
138 | 151 | ], | 151 | ], |
139 | 152 | "../images/project.png": [ | 152 | "../images/project.png": [ |
140 | 153 | 0, | 153 | 0, |
141 | @@ -173,17 +173,13 @@ | |||
142 | 173 | 0, | 173 | 0, |
143 | 174 | -3768 | 174 | -3768 |
144 | 175 | ], | 175 | ], |
145 | 176 | "../images/stop.png": [ | ||
146 | 177 | 0, | ||
147 | 178 | -11310 | ||
148 | 179 | ], | ||
149 | 180 | "../images/person-logo.png": [ | 176 | "../images/person-logo.png": [ |
150 | 181 | 0, | 177 | 0, |
152 | 182 | -19288 | 178 | -19270 |
153 | 183 | ], | 179 | ], |
154 | 184 | "../images/distribution-logo.png": [ | 180 | "../images/distribution-logo.png": [ |
155 | 185 | 0, | 181 | 0, |
157 | 186 | -18646 | 182 | -18628 |
158 | 187 | ], | 183 | ], |
159 | 188 | "../images/retry.png": [ | 184 | "../images/retry.png": [ |
160 | 189 | 0, | 185 | 0, |
161 | @@ -199,7 +195,7 @@ | |||
162 | 199 | ], | 195 | ], |
163 | 200 | "../images/merge-proposal-icon.png": [ | 196 | "../images/merge-proposal-icon.png": [ |
164 | 201 | 0, | 197 | 0, |
166 | 202 | -12786 | 198 | -12950 |
167 | 203 | ], | 199 | ], |
168 | 204 | "../images/download.png": [ | 200 | "../images/download.png": [ |
169 | 205 | 0, | 201 | 0, |
170 | @@ -207,7 +203,7 @@ | |||
171 | 207 | ], | 203 | ], |
172 | 208 | "../images/arrowDown.png": [ | 204 | "../images/arrowDown.png": [ |
173 | 209 | 0, | 205 | 0, |
175 | 210 | -13934 | 206 | -14098 |
176 | 211 | ], | 207 | ], |
177 | 212 | "../images/package-binary.png": [ | 208 | "../images/package-binary.png": [ |
178 | 213 | 0, | 209 | 0, |
179 | @@ -219,11 +215,11 @@ | |||
180 | 219 | ], | 215 | ], |
181 | 220 | "../images/bug-status-expand.png": [ | 216 | "../images/bug-status-expand.png": [ |
182 | 221 | 0, | 217 | 0, |
184 | 222 | -12622 | 218 | -12786 |
185 | 223 | ], | 219 | ], |
186 | 224 | "../images/crowd-large.png": [ | 220 | "../images/crowd-large.png": [ |
187 | 225 | 0, | 221 | 0, |
189 | 226 | -16430 | 222 | -16594 |
190 | 227 | ], | 223 | ], |
191 | 228 | "../images/blueprint.png": [ | 224 | "../images/blueprint.png": [ |
192 | 229 | 0, | 225 | 0, |
193 | @@ -245,9 +241,13 @@ | |||
194 | 245 | 0, | 241 | 0, |
195 | 246 | -6228 | 242 | -6228 |
196 | 247 | ], | 243 | ], |
197 | 244 | "../images/stop.png": [ | ||
198 | 245 | 0, | ||
199 | 246 | -11310 | ||
200 | 247 | ], | ||
201 | 248 | "../images/flame-large.png": [ | 248 | "../images/flame-large.png": [ |
202 | 249 | 0, | 249 | 0, |
204 | 250 | -16976 | 250 | -17140 |
205 | 251 | ], | 251 | ], |
206 | 252 | "../images/bug-dupe-icon.png": [ | 252 | "../images/bug-dupe-icon.png": [ |
207 | 253 | 0, | 253 | 0, |
208 | @@ -259,11 +259,11 @@ | |||
209 | 259 | ], | 259 | ], |
210 | 260 | "../images/build-success.png": [ | 260 | "../images/build-success.png": [ |
211 | 261 | 0, | 261 | 0, |
213 | 262 | -13114 | 262 | -13278 |
214 | 263 | ], | 263 | ], |
215 | 264 | "../images/haspatch-icon.png": [ | 264 | "../images/haspatch-icon.png": [ |
216 | 265 | 0, | 265 | 0, |
218 | 266 | -15738 | 266 | -15902 |
219 | 267 | ], | 267 | ], |
220 | 268 | "../images/person-inactive-badge.png": [ | 268 | "../images/person-inactive-badge.png": [ |
221 | 269 | 0, | 269 | 0, |
222 | @@ -279,7 +279,7 @@ | |||
223 | 279 | ], | 279 | ], |
224 | 280 | "../images/team-logo.png": [ | 280 | "../images/team-logo.png": [ |
225 | 281 | 0, | 281 | 0, |
227 | 282 | -19716 | 282 | -19698 |
228 | 283 | ], | 283 | ], |
229 | 284 | "../images/arrowRight.png": [ | 284 | "../images/arrowRight.png": [ |
230 | 285 | 0, | 285 | 0, |
231 | @@ -311,7 +311,7 @@ | |||
232 | 311 | ], | 311 | ], |
233 | 312 | "../images/arrowUp.png": [ | 312 | "../images/arrowUp.png": [ |
234 | 313 | 0, | 313 | 0, |
236 | 314 | -13770 | 314 | -13934 |
237 | 315 | ], | 315 | ], |
238 | 316 | "../images/distribution.png": [ | 316 | "../images/distribution.png": [ |
239 | 317 | 0, | 317 | 0, |
240 | @@ -319,11 +319,11 @@ | |||
241 | 319 | ], | 319 | ], |
242 | 320 | "../images/error-large.png": [ | 320 | "../images/error-large.png": [ |
243 | 321 | 0, | 321 | 0, |
245 | 322 | -16794 | 322 | -16958 |
246 | 323 | ], | 323 | ], |
247 | 324 | "../images/news.png": [ | 324 | "../images/news.png": [ |
248 | 325 | 0, | 325 | 0, |
250 | 326 | -15574 | 326 | -15738 |
251 | 327 | ], | 327 | ], |
252 | 328 | "../images/treeExpanded.png": [ | 328 | "../images/treeExpanded.png": [ |
253 | 329 | 0, | 329 | 0, |
254 | @@ -331,7 +331,7 @@ | |||
255 | 331 | ], | 331 | ], |
256 | 332 | "../images/build-depwait.png": [ | 332 | "../images/build-depwait.png": [ |
257 | 333 | 0, | 333 | 0, |
259 | 334 | -13606 | 334 | -13770 |
260 | 335 | ], | 335 | ], |
261 | 336 | "../images/blueprint-essential.png": [ | 336 | "../images/blueprint-essential.png": [ |
262 | 337 | 0, | 337 | 0, |
263 | @@ -351,7 +351,7 @@ | |||
264 | 351 | ], | 351 | ], |
265 | 352 | "../images/product-logo.png": [ | 352 | "../images/product-logo.png": [ |
266 | 353 | 0, | 353 | 0, |
268 | 354 | -19074 | 354 | -19056 |
269 | 355 | ], | 355 | ], |
270 | 356 | "../images/blueprint-medium.png": [ | 356 | "../images/blueprint-medium.png": [ |
271 | 357 | 0, | 357 | 0, |
272 | @@ -367,11 +367,11 @@ | |||
273 | 367 | ], | 367 | ], |
274 | 368 | "../images/launchpad-logo.png": [ | 368 | "../images/launchpad-logo.png": [ |
275 | 369 | 0, | 369 | 0, |
277 | 370 | -18432 | 370 | -18414 |
278 | 371 | ], | 371 | ], |
279 | 372 | "../images/flame-logo.png": [ | 372 | "../images/flame-logo.png": [ |
280 | 373 | 0, | 373 | 0, |
282 | 374 | -20144 | 374 | -20126 |
283 | 375 | ], | 375 | ], |
284 | 376 | "../images/translation-template.png": [ | 376 | "../images/translation-template.png": [ |
285 | 377 | 0, | 377 | 0, |
286 | @@ -383,7 +383,7 @@ | |||
287 | 383 | ], | 383 | ], |
288 | 384 | "../images/meeting-logo.png": [ | 384 | "../images/meeting-logo.png": [ |
289 | 385 | 0, | 385 | 0, |
291 | 386 | -19930 | 386 | -19912 |
292 | 387 | ], | 387 | ], |
293 | 388 | "../images/treeCollapsed.png": [ | 388 | "../images/treeCollapsed.png": [ |
294 | 389 | 0, | 389 | 0, |
295 | @@ -391,19 +391,19 @@ | |||
296 | 391 | ], | 391 | ], |
297 | 392 | "../images/green-bar.png": [ | 392 | "../images/green-bar.png": [ |
298 | 393 | 0, | 393 | 0, |
300 | 394 | -15082 | 394 | -15246 |
301 | 395 | ], | 395 | ], |
302 | 396 | "../images/build-superseded.png": [ | 396 | "../images/build-superseded.png": [ |
303 | 397 | 0, | 397 | 0, |
305 | 398 | -12950 | 398 | -13114 |
306 | 399 | ], | 399 | ], |
307 | 400 | "../images/trash-large.png": [ | 400 | "../images/trash-large.png": [ |
308 | 401 | 0, | 401 | 0, |
310 | 402 | -18068 | 402 | -18050 |
311 | 403 | ], | 403 | ], |
312 | 404 | "../images/red-bar.png": [ | 404 | "../images/red-bar.png": [ |
313 | 405 | 0, | 405 | 0, |
315 | 406 | -15410 | 406 | -15574 |
316 | 407 | ], | 407 | ], |
317 | 408 | "../images/add.png": [ | 408 | "../images/add.png": [ |
318 | 409 | 0, | 409 | 0, |
319 | @@ -413,9 +413,13 @@ | |||
320 | 413 | 0, | 413 | 0, |
321 | 414 | -328 | 414 | -328 |
322 | 415 | ], | 415 | ], |
323 | 416 | "../images/read-only.png": [ | ||
324 | 417 | 0, | ||
325 | 418 | -9998 | ||
326 | 419 | ], | ||
327 | 416 | "../images/person-inactive-logo.png": [ | 420 | "../images/person-inactive-logo.png": [ |
328 | 417 | 0, | 421 | 0, |
330 | 418 | -19502 | 422 | -19484 |
331 | 419 | ], | 423 | ], |
332 | 420 | "../images/edit.png": [ | 424 | "../images/edit.png": [ |
333 | 421 | 0, | 425 | 0, |
334 | @@ -427,11 +431,11 @@ | |||
335 | 427 | ], | 431 | ], |
336 | 428 | "../images/warning-large.png": [ | 432 | "../images/warning-large.png": [ |
337 | 429 | 0, | 433 | 0, |
339 | 430 | -16248 | 434 | -16412 |
340 | 431 | ], | 435 | ], |
341 | 432 | "../images/arrowEnd.png": [ | 436 | "../images/arrowEnd.png": [ |
342 | 433 | 0, | 437 | 0, |
344 | 434 | -14262 | 438 | -14426 |
345 | 435 | ], | 439 | ], |
346 | 436 | "../images/cve.png": [ | 440 | "../images/cve.png": [ |
347 | 437 | 0, | 441 | 0, |
348 | @@ -439,7 +443,7 @@ | |||
349 | 439 | ], | 443 | ], |
350 | 440 | "../images/merge-proposal-large.png": [ | 444 | "../images/merge-proposal-large.png": [ |
351 | 441 | 0, | 445 | 0, |
353 | 442 | -17886 | 446 | -17868 |
354 | 443 | ], | 447 | ], |
355 | 444 | "../images/branch.png": [ | 448 | "../images/branch.png": [ |
356 | 445 | 0, | 449 | 0, |
357 | @@ -469,4 +473,4 @@ | |||
358 | 469 | 0, | 473 | 0, |
359 | 470 | -1148 | 474 | -1148 |
360 | 471 | ] | 475 | ] |
362 | 472 | } | 476 | } |
363 | 473 | \ No newline at end of file | 477 | \ No newline at end of file |
364 | 474 | 478 | ||
365 | === modified file 'lib/canonical/launchpad/icing/style-3-0.css.in' | |||
366 | --- lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-14 07:33:54 +0000 | |||
367 | +++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-23 10:25:23 +0000 | |||
368 | @@ -1471,10 +1471,10 @@ | |||
369 | 1471 | Colors and fonts | 1471 | Colors and fonts |
370 | 1472 | */ | 1472 | */ |
371 | 1473 | 1473 | ||
376 | 1474 | h1, h2, h3, | 1474 | h1, h2, h3, |
377 | 1475 | table.listing thead, | 1475 | table.listing thead, |
378 | 1476 | #homepage-stats strong, | 1476 | #homepage-stats strong, |
379 | 1477 | #application-footer strong, | 1477 | #application-footer strong, |
380 | 1478 | #application-summary strong { | 1478 | #application-summary strong { |
381 | 1479 | color: #5a5a5a; | 1479 | color: #5a5a5a; |
382 | 1480 | } | 1480 | } |
383 | @@ -2204,7 +2204,10 @@ | |||
384 | 2204 | background-image: url(/@@/ppa-icon-inactive.png); /* sprite-ref: icon-sprites */ | 2204 | background-image: url(/@@/ppa-icon-inactive.png); /* sprite-ref: icon-sprites */ |
385 | 2205 | background-repeat: no-repeat; | 2205 | background-repeat: no-repeat; |
386 | 2206 | } | 2206 | } |
388 | 2207 | 2207 | .source-package-recipe { | |
389 | 2208 | background-image: url(/@@/source-package-recipe.png); /* sprite-ref: icon-sprites */ | ||
390 | 2209 | background-repeat: no-repeat; | ||
391 | 2210 | } | ||
392 | 2208 | .bug-status-expand { | 2211 | .bug-status-expand { |
393 | 2209 | background-image: url(/@@/bug-status-expand.png); /* sprite-ref: icon-sprites */ | 2212 | background-image: url(/@@/bug-status-expand.png); /* sprite-ref: icon-sprites */ |
394 | 2210 | background-repeat: no-repeat; | 2213 | background-repeat: no-repeat; |
395 | 2211 | 2214 | ||
396 | === modified file 'lib/lp/code/browser/configure.zcml' | |||
397 | --- lib/lp/code/browser/configure.zcml 2011-02-10 04:16:09 +0000 | |||
398 | +++ lib/lp/code/browser/configure.zcml 2011-02-23 10:25:23 +0000 | |||
399 | @@ -1239,6 +1239,12 @@ | |||
400 | 1239 | name="+builds" | 1239 | name="+builds" |
401 | 1240 | template="../templates/sourcepackagerecipe-builds.pt" | 1240 | template="../templates/sourcepackagerecipe-builds.pt" |
402 | 1241 | permission="launchpad.View"/> | 1241 | permission="launchpad.View"/> |
403 | 1242 | <browser:page | ||
404 | 1243 | for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe" | ||
405 | 1244 | layer="lp.code.publisher.CodeLayer" | ||
406 | 1245 | class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeRequestDailyBuildView" | ||
407 | 1246 | name="+request-daily-build" | ||
408 | 1247 | permission="launchpad.Edit"/> | ||
409 | 1242 | </facet> | 1248 | </facet> |
410 | 1243 | <facet facet="branches"> | 1249 | <facet facet="branches"> |
411 | 1244 | <browser:defaultView | 1250 | <browser:defaultView |
412 | 1245 | 1251 | ||
413 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' | |||
414 | --- lib/lp/code/browser/sourcepackagerecipe.py 2011-02-22 09:07:44 +0000 | |||
415 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000 | |||
416 | @@ -180,12 +180,27 @@ | |||
417 | 180 | 180 | ||
418 | 181 | facet = 'branches' | 181 | facet = 'branches' |
419 | 182 | 182 | ||
421 | 183 | links = ('request_builds',) | 183 | links = ('request_builds', 'request_daily_build',) |
422 | 184 | 184 | ||
423 | 185 | def request_builds(self): | 185 | def request_builds(self): |
424 | 186 | """Provide a link for requesting builds of a recipe.""" | 186 | """Provide a link for requesting builds of a recipe.""" |
425 | 187 | return Link('+request-builds', 'Request build(s)', icon='add') | 187 | return Link('+request-builds', 'Request build(s)', icon='add') |
426 | 188 | 188 | ||
427 | 189 | def request_daily_build(self): | ||
428 | 190 | """Provide a link for requesting a daily build of a recipe.""" | ||
429 | 191 | recipe = self.context | ||
430 | 192 | ppa = recipe.daily_build_archive | ||
431 | 193 | if (ppa is None or not recipe.build_daily or not recipe.is_stale | ||
432 | 194 | or not recipe.distroseries): | ||
433 | 195 | show_request_build = False | ||
434 | 196 | else: | ||
435 | 197 | has_upload = ppa.checkArchivePermission(recipe.owner) | ||
436 | 198 | show_request_build = has_upload | ||
437 | 199 | |||
438 | 200 | return Link( | ||
439 | 201 | '+request-daily-build', 'Build now', | ||
440 | 202 | enabled=show_request_build) | ||
441 | 203 | |||
442 | 189 | 204 | ||
443 | 190 | class SourcePackageRecipeView(LaunchpadView): | 205 | class SourcePackageRecipeView(LaunchpadView): |
444 | 191 | """Default view of a SourcePackageRecipe.""" | 206 | """Default view of a SourcePackageRecipe.""" |
445 | @@ -292,6 +307,17 @@ | |||
446 | 292 | return builds | 307 | return builds |
447 | 293 | 308 | ||
448 | 294 | 309 | ||
449 | 310 | def new_builds_notification_text(builds): | ||
450 | 311 | nr_builds = len(builds) | ||
451 | 312 | if not nr_builds: | ||
452 | 313 | builds_text = "All requested recipe builds are already queued." | ||
453 | 314 | elif nr_builds == 1: | ||
454 | 315 | builds_text = "1 new recipe build has been queued." | ||
455 | 316 | else: | ||
456 | 317 | builds_text = "%d new recipe builds have been queued." % nr_builds | ||
457 | 318 | return builds_text | ||
458 | 319 | |||
459 | 320 | |||
460 | 295 | class SourcePackageRecipeRequestBuildsView(LaunchpadFormView): | 321 | class SourcePackageRecipeRequestBuildsView(LaunchpadFormView): |
461 | 296 | """A view for requesting builds of a SourcePackageRecipe.""" | 322 | """A view for requesting builds of a SourcePackageRecipe.""" |
462 | 297 | 323 | ||
463 | @@ -341,14 +367,16 @@ | |||
464 | 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. |
465 | 342 | """ | 368 | """ |
466 | 343 | errors = {} | 369 | errors = {} |
467 | 370 | builds = [] | ||
468 | 344 | for distroseries in data['distros']: | 371 | for distroseries in data['distros']: |
469 | 345 | try: | 372 | try: |
471 | 346 | self.context.requestBuild( | 373 | build = self.context.requestBuild( |
472 | 347 | data['archive'], self.user, distroseries, manual=True) | 374 | data['archive'], self.user, distroseries, manual=True) |
473 | 375 | builds.append(build) | ||
474 | 348 | except BuildAlreadyPending, e: | 376 | except BuildAlreadyPending, e: |
475 | 349 | errors['distros'] = ("An identical build is already pending " | 377 | errors['distros'] = ("An identical build is already pending " |
476 | 350 | "for %s." % e.distroseries) | 378 | "for %s." % e.distroseries) |
478 | 351 | return errors | 379 | return builds, errors |
479 | 352 | 380 | ||
480 | 353 | 381 | ||
481 | 354 | class SourcePackageRecipeRequestBuildsHtmlView( | 382 | class SourcePackageRecipeRequestBuildsHtmlView( |
482 | @@ -367,44 +395,120 @@ | |||
483 | 367 | 395 | ||
484 | 368 | @action('Request builds', name='request') | 396 | @action('Request builds', name='request') |
485 | 369 | def request_action(self, action, data): | 397 | def request_action(self, action, data): |
487 | 370 | errors = self.requestBuild(data) | 398 | builds, errors = self.requestBuild(data) |
488 | 371 | if errors: | 399 | if errors: |
489 | 372 | [self.setFieldError(field, message) | 400 | [self.setFieldError(field, message) |
490 | 373 | for (field, message) in errors.items()] | 401 | for (field, message) in errors.items()] |
491 | 374 | return | 402 | return |
492 | 375 | self.next_url = self.cancel_url | 403 | self.next_url = self.cancel_url |
525 | 376 | 404 | self.request.response.addNotification( | |
526 | 377 | 405 | new_builds_notification_text(builds)) | |
527 | 378 | class SourcePackageRecipeRequestBuildsAjaxView( | 406 | |
528 | 379 | SourcePackageRecipeRequestBuildsView): | 407 | |
529 | 380 | """Supports AJAX form recipe build requests.""" | 408 | class SourcePackageRecipeRequestBuildsAjaxView( |
530 | 381 | 409 | SourcePackageRecipeRequestBuildsView): | |
531 | 382 | def _process_error(self, data, errors, reason): | 410 | """Supports AJAX form recipe build requests.""" |
532 | 383 | """Set up the response and json data to return to the caller.""" | 411 | |
533 | 384 | self.request.response.setStatus(400, reason) | 412 | def _process_error(self, data, errors, reason): |
534 | 385 | self.request.response.setHeader('Content-type', 'application/json') | 413 | """Set up the response and json data to return to the caller.""" |
535 | 386 | return simplejson.dumps(errors) | 414 | self.request.response.setStatus(400, reason) |
536 | 387 | 415 | self.request.response.setHeader('Content-type', 'application/json') | |
537 | 388 | def failure(self, action, data, errors): | 416 | return simplejson.dumps(errors) |
538 | 389 | """Called by the form if validate() finds any errors. | 417 | |
539 | 390 | 418 | def failure(self, action, data, errors): | |
540 | 391 | We simply convert the errors to json and return that data to the | 419 | """Called by the form if validate() finds any errors. |
541 | 392 | caller for display to the user. | 420 | |
542 | 393 | """ | 421 | We simply convert the errors to json and return that data to the |
543 | 394 | return self._process_error(data, self.widget_errors, "Validation") | 422 | caller for display to the user. |
544 | 395 | 423 | """ | |
545 | 396 | @action('Request builds', name='request', failure=failure) | 424 | return self._process_error(data, self.widget_errors, "Validation") |
546 | 397 | def request_action(self, action, data): | 425 | |
547 | 398 | """User action for requesting a number of builds. | 426 | @action('Request builds', name='request', failure=failure) |
548 | 399 | 427 | def request_action(self, action, data): | |
549 | 400 | The failure handler will handle any validation errors. We still need | 428 | """User action for requesting a number of builds. |
550 | 401 | to handle errors which may occur when invoking the business logic. | 429 | |
551 | 402 | These "expected" errors are ones which result in a predefined message | 430 | The failure handler will handle any validation errors. We still need |
552 | 403 | being displayed to the user. If the business method raises an | 431 | to handle errors which may occur when invoking the business logic. |
553 | 404 | unexpected exception, that will be handled using the form's standard | 432 | These "expected" errors are ones which result in a predefined message |
554 | 405 | exception processing mechanism (using response code 500). | 433 | being displayed to the user. If the business method raises an |
555 | 406 | """ | 434 | unexpected exception, that will be handled using the form's standard |
556 | 407 | errors = self.requestBuild(data) | 435 | exception processing mechanism (using response code 500). |
557 | 436 | """ | ||
558 | 437 | builds, errors = self.requestBuild(data) | ||
559 | 438 | # If there are errors we return a json data snippet containing the | ||
560 | 439 | # errors instead of rendering the form. These errors are processed | ||
561 | 440 | # by the caller's response handler and displayed to the user. | ||
562 | 441 | if errors: | ||
563 | 442 | return self._process_error(data, errors, "Request Build") | ||
564 | 443 | |||
565 | 444 | @property | ||
566 | 445 | def builds(self): | ||
567 | 446 | return builds_for_recipe(self.context) | ||
568 | 447 | |||
569 | 448 | |||
570 | 449 | class SourcePackageRecipeRequestDailyBuildView(LaunchpadFormView): | ||
571 | 450 | """Supports requests to perform a daily build for a recipe. | ||
572 | 451 | |||
573 | 452 | Renders the recipe builds table so that the recipe index page can be | ||
574 | 453 | updated with the new build records. | ||
575 | 454 | |||
576 | 455 | This view works for both ajax and html form requests. | ||
577 | 456 | """ | ||
578 | 457 | |||
579 | 458 | # Attributes for the html version | ||
580 | 459 | page_title = "Build now" | ||
581 | 460 | |||
582 | 461 | class schema(Interface): | ||
583 | 462 | """Schema for requesting a build.""" | ||
584 | 463 | |||
585 | 464 | @action('Build now', name='build') | ||
586 | 465 | def build_action(self, action, data): | ||
587 | 466 | recipe = self.context | ||
588 | 467 | builds = recipe.performDailyBuild() | ||
589 | 468 | if self.request.is_ajax: | ||
590 | 469 | template = ViewPageTemplateFile( | ||
591 | 470 | "../templates/sourcepackagerecipe-builds.pt") | ||
592 | 471 | return template(self) | ||
593 | 472 | else: | ||
594 | 473 | self.next_url = canonical_url(recipe) | ||
595 | 474 | self.request.response.addNotification( | ||
596 | 475 | new_builds_notification_text(builds)) | ||
597 | 476 | |||
598 | 477 | @property | ||
599 | 478 | def builds(self): | ||
600 | 479 | return builds_for_recipe(self.context) | ||
601 | 480 | |||
602 | 481 | |||
603 | 482 | class SourcePackageRecipeRequestBuildsAjaxView( | ||
604 | 483 | SourcePackageRecipeRequestBuildsView): | ||
605 | 484 | """Supports AJAX form recipe build requests.""" | ||
606 | 485 | |||
607 | 486 | def _process_error(self, data, errors, reason): | ||
608 | 487 | """Set up the response and json data to return to the caller.""" | ||
609 | 488 | self.request.response.setStatus(400, reason) | ||
610 | 489 | self.request.response.setHeader('Content-type', 'application/json') | ||
611 | 490 | return simplejson.dumps(errors) | ||
612 | 491 | |||
613 | 492 | def failure(self, action, data, errors): | ||
614 | 493 | """Called by the form if validate() finds any errors. | ||
615 | 494 | |||
616 | 495 | We simply convert the errors to json and return that data to the | ||
617 | 496 | caller for display to the user. | ||
618 | 497 | """ | ||
619 | 498 | return self._process_error(data, self.widget_errors, "Validation") | ||
620 | 499 | |||
621 | 500 | @action('Request builds', name='request', failure=failure) | ||
622 | 501 | def request_action(self, action, data): | ||
623 | 502 | """User action for requesting a number of builds. | ||
624 | 503 | |||
625 | 504 | The failure handler will handle any validation errors. We still need | ||
626 | 505 | to handle errors which may occur when invoking the business logic. | ||
627 | 506 | These "expected" errors are ones which result in a predefined message | ||
628 | 507 | being displayed to the user. If the business method raises an | ||
629 | 508 | unexpected exception, that will be handled using the form's standard | ||
630 | 509 | exception processing mechanism (using response code 500). | ||
631 | 510 | """ | ||
632 | 511 | builds, errors = self.requestBuild(data) | ||
633 | 408 | # If there are errors we return a json data snippet containing the | 512 | # If there are errors we return a json data snippet containing the |
634 | 409 | # errors instead of rendering the form. These errors are processed | 513 | # errors instead of rendering the form. These errors are processed |
635 | 410 | # by the caller's response handler and displayed to the user. | 514 | # by the caller's response handler and displayed to the user. |
636 | 411 | 515 | ||
637 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' | |||
638 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-21 20:55:00 +0000 | |||
639 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-23 10:25:23 +0000 | |||
640 | @@ -26,6 +26,7 @@ | |||
641 | 26 | from canonical.launchpad.testing.pages import ( | 26 | from canonical.launchpad.testing.pages import ( |
642 | 27 | extract_text, | 27 | extract_text, |
643 | 28 | find_main_content, | 28 | find_main_content, |
644 | 29 | find_tag_by_id, | ||
645 | 29 | find_tags_by_class, | 30 | find_tags_by_class, |
646 | 30 | get_feedback_messages, | 31 | get_feedback_messages, |
647 | 31 | get_radio_button_text_for_field, | 32 | get_radio_button_text_for_field, |
648 | @@ -1227,6 +1228,88 @@ | |||
649 | 1227 | self.assertEqual( | 1228 | self.assertEqual( |
650 | 1228 | [build1, build2, build3, build4, build5], view.builds) | 1229 | [build1, build2, build3, build4, build5], view.builds) |
651 | 1229 | 1230 | ||
652 | 1231 | def test_request_daily_builds_button_stale(self): | ||
653 | 1232 | # Recipes that are stale and are built daily have a build now link | ||
654 | 1233 | recipe = self.factory.makeSourcePackageRecipe( | ||
655 | 1234 | owner=self.chef, daily_build_archive=self.ppa, | ||
656 | 1235 | is_stale=True, build_daily=True) | ||
657 | 1236 | browser = self.getViewBrowser(recipe) | ||
658 | 1237 | build_button = find_tag_by_id(browser.contents, 'field.actions.build') | ||
659 | 1238 | self.assertIsNot(None, build_button) | ||
660 | 1239 | |||
661 | 1240 | def test_request_daily_builds_button_not_stale(self): | ||
662 | 1241 | # Recipes that are not stale do not have a build now link | ||
663 | 1242 | login(ANONYMOUS) | ||
664 | 1243 | recipe = self.factory.makeSourcePackageRecipe( | ||
665 | 1244 | owner=self.chef, daily_build_archive=self.ppa, | ||
666 | 1245 | is_stale=False, build_daily=True) | ||
667 | 1246 | browser = self.getViewBrowser(recipe) | ||
668 | 1247 | build_button = find_tag_by_id(browser.contents, 'field.actions.build') | ||
669 | 1248 | self.assertIs(None, build_button) | ||
670 | 1249 | |||
671 | 1250 | def test_request_daily_builds_button_not_daily(self): | ||
672 | 1251 | # Recipes that are not built daily do not have a build now link | ||
673 | 1252 | login(ANONYMOUS) | ||
674 | 1253 | recipe = self.factory.makeSourcePackageRecipe( | ||
675 | 1254 | owner=self.chef, daily_build_archive=self.ppa, | ||
676 | 1255 | is_stale=True, build_daily=False) | ||
677 | 1256 | browser = self.getViewBrowser(recipe) | ||
678 | 1257 | build_button = find_tag_by_id(browser.contents, 'field.actions.build') | ||
679 | 1258 | self.assertIs(None, build_button) | ||
680 | 1259 | |||
681 | 1260 | def test_request_daily_builds_button_no_daily_ppa(self): | ||
682 | 1261 | # Recipes that have no daily build ppa do not have a build now link | ||
683 | 1262 | login(ANONYMOUS) | ||
684 | 1263 | recipe = self.factory.makeSourcePackageRecipe( | ||
685 | 1264 | owner=self.chef, is_stale=True, build_daily=True) | ||
686 | 1265 | naked_recipe = removeSecurityProxy(recipe) | ||
687 | 1266 | naked_recipe.daily_build_archive = None | ||
688 | 1267 | browser = self.getViewBrowser(recipe) | ||
689 | 1268 | build_button = find_tag_by_id(browser.contents, 'field.actions.build') | ||
690 | 1269 | self.assertIs(None, build_button) | ||
691 | 1270 | |||
692 | 1271 | def test_request_daily_builds_button_ppa_with_no_permissions(self): | ||
693 | 1272 | # Recipes that have a daily build ppa without upload permissions | ||
694 | 1273 | # do not have a build now link | ||
695 | 1274 | login(ANONYMOUS) | ||
696 | 1275 | distroseries = self.factory.makeSourcePackageRecipeDistroseries() | ||
697 | 1276 | person = self.factory.makePerson() | ||
698 | 1277 | daily_build_archive = self.factory.makeArchive( | ||
699 | 1278 | distribution=distroseries.distribution, owner=person) | ||
700 | 1279 | recipe = self.factory.makeSourcePackageRecipe( | ||
701 | 1280 | owner=self.chef, daily_build_archive=daily_build_archive, | ||
702 | 1281 | is_stale=True, build_daily=True) | ||
703 | 1282 | browser = self.getViewBrowser(recipe) | ||
704 | 1283 | build_button = find_tag_by_id(browser.contents, 'field.actions.build') | ||
705 | 1284 | self.assertIs(None, build_button) | ||
706 | 1285 | |||
707 | 1286 | def test_request_daily_builds_ajax_link_not_rendered(self): | ||
708 | 1287 | # The Build now link should not be rendered without javascript. | ||
709 | 1288 | recipe = self.factory.makeSourcePackageRecipe( | ||
710 | 1289 | owner=self.chef, daily_build_archive=self.ppa, | ||
711 | 1290 | is_stale=True, build_daily=True) | ||
712 | 1291 | browser = self.getViewBrowser(recipe) | ||
713 | 1292 | build_link = find_tag_by_id(browser.contents, 'request-daily-builds') | ||
714 | 1293 | self.assertIs(None, build_link) | ||
715 | 1294 | |||
716 | 1295 | def test_request_daily_builds_action(self): | ||
717 | 1296 | # Daily builds should be triggered when requested. | ||
718 | 1297 | recipe = self.factory.makeSourcePackageRecipe( | ||
719 | 1298 | owner=self.chef, daily_build_archive=self.ppa, | ||
720 | 1299 | is_stale=True, build_daily=True) | ||
721 | 1300 | browser = self.getViewBrowser(recipe) | ||
722 | 1301 | browser.getControl('Build now').click() | ||
723 | 1302 | login(ANONYMOUS) | ||
724 | 1303 | builds = recipe.getPendingBuilds() | ||
725 | 1304 | build_distros = [ | ||
726 | 1305 | build.distroseries.displayname for build in builds] | ||
727 | 1306 | build_distros.sort() | ||
728 | 1307 | # Our recipe has a Warty distroseries | ||
729 | 1308 | self.assertEqual(['Warty'], build_distros) | ||
730 | 1309 | self.assertEqual( | ||
731 | 1310 | set([2505]), | ||
732 | 1311 | set(build.buildqueue_record.lastscore for build in builds)) | ||
733 | 1312 | |||
734 | 1230 | def test_request_builds_page(self): | 1313 | def test_request_builds_page(self): |
735 | 1231 | """Ensure the +request-builds page is sane.""" | 1314 | """Ensure the +request-builds page is sane.""" |
736 | 1232 | recipe = self.makeRecipe() | 1315 | recipe = self.makeRecipe() |
737 | 1233 | 1316 | ||
738 | === modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py' | |||
739 | --- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-18 03:39:25 +0000 | |||
740 | +++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000 | |||
741 | @@ -136,6 +136,10 @@ | |||
742 | 136 | able to upload to the archive. | 136 | able to upload to the archive. |
743 | 137 | """ | 137 | """ |
744 | 138 | 138 | ||
745 | 139 | @export_write_operation() | ||
746 | 140 | def performDailyBuild(): | ||
747 | 141 | """Perform a build into the daily build archive.""" | ||
748 | 142 | |||
749 | 139 | 143 | ||
750 | 140 | class ISourcePackageRecipeEdit(Interface): | 144 | class ISourcePackageRecipeEdit(Interface): |
751 | 141 | """ISourcePackageRecipe methods that require launchpad.Edit permission.""" | 145 | """ISourcePackageRecipe methods that require launchpad.Edit permission.""" |
752 | 142 | 146 | ||
753 | === modified file 'lib/lp/code/javascript/requestbuild_overlay.js' | |||
754 | --- lib/lp/code/javascript/requestbuild_overlay.js 2011-02-18 01:06:19 +0000 | |||
755 | +++ lib/lp/code/javascript/requestbuild_overlay.js 2011-02-23 10:25:23 +0000 | |||
756 | @@ -12,7 +12,8 @@ | |||
757 | 12 | 12 | ||
758 | 13 | var lp_client; | 13 | var lp_client; |
759 | 14 | var request_build_overlay = null; | 14 | var request_build_overlay = null; |
761 | 15 | var response_handler; | 15 | var request_build_response_handler; |
762 | 16 | var request_daily_build_response_handler; | ||
763 | 16 | 17 | ||
764 | 17 | function set_up_lp_client() { | 18 | function set_up_lp_client() { |
765 | 18 | if (lp_client === undefined) { | 19 | if (lp_client === undefined) { |
766 | @@ -55,7 +56,7 @@ | |||
767 | 55 | } | 56 | } |
768 | 56 | }; | 57 | }; |
769 | 57 | 58 | ||
771 | 58 | namespace.connect_requestbuild = function() { | 59 | namespace.connect_requestbuilds = function() { |
772 | 59 | 60 | ||
773 | 60 | var request_build_handle = Y.one('#request-builds'); | 61 | var request_build_handle = Y.one('#request-builds'); |
774 | 61 | request_build_handle.addClass('js-action'); | 62 | request_build_handle.addClass('js-action'); |
775 | @@ -80,26 +81,102 @@ | |||
776 | 80 | request_build_overlay.render(); | 81 | request_build_overlay.render(); |
777 | 81 | } | 82 | } |
778 | 82 | request_build_overlay.clearError(); | 83 | request_build_overlay.clearError(); |
780 | 83 | var temp_spinner = [ | 84 | var loading_spinner = [ |
781 | 84 | '<div id="temp-spinner">', | 85 | '<div id="temp-spinner">', |
782 | 85 | '<img src="/@@/spinner"/>Loading...', | 86 | '<img src="/@@/spinner"/>Loading...', |
783 | 86 | '</div>'].join(''); | 87 | '</div>'].join(''); |
785 | 87 | request_build_overlay.form_node.set("innerHTML", temp_spinner); | 88 | request_build_overlay.form_node.set("innerHTML", loading_spinner); |
786 | 88 | request_build_overlay.loadFormContentAndRender('+builds/++form++'); | 89 | request_build_overlay.loadFormContentAndRender('+builds/++form++'); |
787 | 89 | request_build_overlay.show(); | 90 | request_build_overlay.show(); |
788 | 90 | }); | 91 | }); |
789 | 91 | 92 | ||
790 | 92 | // Wire up the processing hooks | 93 | // Wire up the processing hooks |
793 | 93 | response_handler = new RequestResponseHandler(); | 94 | request_build_response_handler = new RequestResponseHandler(); |
794 | 94 | response_handler.clearProgressUI = function() { | 95 | request_build_response_handler.clearProgressUI = function() { |
795 | 95 | destroy_temporary_spinner(); | 96 | destroy_temporary_spinner(); |
796 | 96 | }; | 97 | }; |
798 | 97 | response_handler.showError = function(error) { | 98 | request_build_response_handler.showError = function(error) { |
799 | 98 | request_build_overlay.showError(error); | 99 | request_build_overlay.showError(error); |
800 | 99 | Y.log(error); | 100 | Y.log(error); |
801 | 100 | }; | 101 | }; |
802 | 101 | }; | 102 | }; |
803 | 102 | 103 | ||
804 | 104 | var NO_BUILDS_MESSAGE = "All requested recipe builds are already queued."; | ||
805 | 105 | var ONE_BUILD_MESSAGE = "1 new recipe build has been queued."; | ||
806 | 106 | var MANY_BUILDS_MESSAGE = "{nr_new} new recipe builds have been queued."; | ||
807 | 107 | |||
808 | 108 | namespace.connect_requestdailybuild = function() { | ||
809 | 109 | |||
810 | 110 | var request_daily_build_handle = Y.one('#request-daily-build'); | ||
811 | 111 | request_daily_build_handle.on('click', function(e) { | ||
812 | 112 | e.preventDefault(); | ||
813 | 113 | |||
814 | 114 | create_temporary_spinner( | ||
815 | 115 | "Requesting build...", request_daily_build_handle); | ||
816 | 116 | request_daily_build_handle.addClass("unseen"); | ||
817 | 117 | |||
818 | 118 | var base_url = LP.client.cache.context.web_link; | ||
819 | 119 | var submit_url = base_url+"/+request-daily-build"; | ||
820 | 120 | var current_builds = harvest_current_build_records(); | ||
821 | 121 | |||
822 | 122 | var qs = LP.client.append_qs('', 'field.actions.build', 'Build now'); | ||
823 | 123 | var y_config = { | ||
824 | 124 | method: "POST", | ||
825 | 125 | headers: {'Accept': 'application/xhtml'}, | ||
826 | 126 | on: { | ||
827 | 127 | failure: request_daily_build_response_handler.getErrorHandler( | ||
828 | 128 | function(handler, id, response) { | ||
829 | 129 | request_daily_build_handle.removeClass("unseen"); | ||
830 | 130 | var server_error = 'Server error, ' + | ||
831 | 131 | 'please contact an administrator.'; | ||
832 | 132 | handler.showError(server_error); | ||
833 | 133 | }), | ||
834 | 134 | success: | ||
835 | 135 | request_daily_build_response_handler.getSuccessHandler( | ||
836 | 136 | function(handler, id, response) { | ||
837 | 137 | var nr_new = display_build_records( | ||
838 | 138 | response.responseText, current_builds); | ||
839 | 139 | var new_builds_message; | ||
840 | 140 | switch (nr_new) { | ||
841 | 141 | case 0: | ||
842 | 142 | new_builds_message = NO_BUILDS_MESSAGE; | ||
843 | 143 | break; | ||
844 | 144 | case 1: | ||
845 | 145 | new_builds_message = ONE_BUILD_MESSAGE; | ||
846 | 146 | break; | ||
847 | 147 | default: | ||
848 | 148 | new_builds_message = | ||
849 | 149 | Y.Lang.substitute( | ||
850 | 150 | MANY_BUILDS_MESSAGE, | ||
851 | 151 | {nr_new: nr_new}); | ||
852 | 152 | } | ||
853 | 153 | var build_message_node = Y.Node.create([ | ||
854 | 154 | '<div id="new-builds-info" class="build-informational">', | ||
855 | 155 | new_builds_message, | ||
856 | 156 | '</div>'].join('')); | ||
857 | 157 | request_daily_build_handle.insert( | ||
858 | 158 | build_message_node, | ||
859 | 159 | request_daily_build_handle); | ||
860 | 160 | Y.later(20000, build_message_node, 'hide', true); | ||
861 | 161 | } | ||
862 | 162 | ) | ||
863 | 163 | }, | ||
864 | 164 | data: qs | ||
865 | 165 | }; | ||
866 | 166 | Y.io(submit_url, y_config); | ||
867 | 167 | }); | ||
868 | 168 | |||
869 | 169 | // Wire up the processing hooks | ||
870 | 170 | request_daily_build_response_handler = new RequestResponseHandler(); | ||
871 | 171 | request_daily_build_response_handler.clearProgressUI = function() { | ||
872 | 172 | destroy_temporary_spinner(); | ||
873 | 173 | }; | ||
874 | 174 | request_daily_build_response_handler.showError = function(error) { | ||
875 | 175 | alert(error); | ||
876 | 176 | Y.log(error); | ||
877 | 177 | }; | ||
878 | 178 | }; | ||
879 | 179 | |||
880 | 103 | /* | 180 | /* |
881 | 104 | * A function to return the current build records as displayed on the page | 181 | * A function to return the current build records as displayed on the page |
882 | 105 | */ | 182 | */ |
883 | @@ -118,14 +195,34 @@ | |||
884 | 118 | } | 195 | } |
885 | 119 | 196 | ||
886 | 120 | /* | 197 | /* |
887 | 198 | * Render build records and flash the new ones | ||
888 | 199 | */ | ||
889 | 200 | function display_build_records(build_records_markup, current_builds) { | ||
890 | 201 | var target = Y.one('#builds-target'); | ||
891 | 202 | target.set('innerHTML', build_records_markup); | ||
892 | 203 | var new_builds = harvest_current_build_records(); | ||
893 | 204 | var nr_new_builds = 0; | ||
894 | 205 | Y.Array.each(new_builds, function(row_id) { | ||
895 | 206 | if( current_builds.indexOf(row_id)>=0 ) | ||
896 | 207 | return; | ||
897 | 208 | nr_new_builds += 1; | ||
898 | 209 | var row = Y.one('#'+row_id); | ||
899 | 210 | var anim = Y.lazr.anim.green_flash({node: row}); | ||
900 | 211 | anim.run(); | ||
901 | 212 | }); | ||
902 | 213 | return nr_new_builds; | ||
903 | 214 | } | ||
904 | 215 | |||
905 | 216 | /* | ||
906 | 121 | * Perform any client side validation | 217 | * Perform any client side validation |
907 | 122 | * Return: true if data is valid | 218 | * Return: true if data is valid |
908 | 123 | */ | 219 | */ |
909 | 124 | function validate(data) { | 220 | function validate(data) { |
910 | 125 | var distros = data['field.distros'] | 221 | var distros = data['field.distros'] |
911 | 126 | if (Y.Object.size(distros) == 0) { | 222 | if (Y.Object.size(distros) == 0) { |
914 | 127 | response_handler.showError("You need to specify at least one " + | 223 | request_build_response_handler.showError( |
915 | 128 | "distro series for which to build."); | 224 | "You need to specify at least one distro series for " + |
916 | 225 | "which to build."); | ||
917 | 129 | return false; | 226 | return false; |
918 | 130 | } | 227 | } |
919 | 131 | return true; | 228 | return true; |
920 | @@ -137,7 +234,9 @@ | |||
921 | 137 | function do_request_builds(data) { | 234 | function do_request_builds(data) { |
922 | 138 | if (!validate(data)) | 235 | if (!validate(data)) |
923 | 139 | return; | 236 | return; |
925 | 140 | create_temporary_spinner(); | 237 | var spinner_location = Y.one('.yui3-lazr-formoverlay-actions'); |
926 | 238 | create_temporary_spinner("Requesting builds...", spinner_location); | ||
927 | 239 | |||
928 | 141 | var base_url = LP.client.cache.context.web_link; | 240 | var base_url = LP.client.cache.context.web_link; |
929 | 142 | var submit_url = base_url+"/+builds"; | 241 | var submit_url = base_url+"/+builds"; |
930 | 143 | var current_builds = harvest_current_build_records(); | 242 | var current_builds = harvest_current_build_records(); |
931 | @@ -145,14 +244,14 @@ | |||
932 | 145 | method: "POST", | 244 | method: "POST", |
933 | 146 | headers: {'Accept': 'application/json; application/xhtml'}, | 245 | headers: {'Accept': 'application/json; application/xhtml'}, |
934 | 147 | on: { | 246 | on: { |
936 | 148 | failure: response_handler.getErrorHandler( | 247 | failure: request_build_response_handler.getErrorHandler( |
937 | 149 | function(handler, id, response) { | 248 | function(handler, id, response) { |
938 | 150 | if( response.status >= 500 ) { | 249 | if( response.status >= 500 ) { |
939 | 151 | // There's some error content we need to display. | 250 | // There's some error content we need to display. |
940 | 152 | request_build_overlay.set( | 251 | request_build_overlay.set( |
941 | 153 | 'form_content', response.responseText); | 252 | 'form_content', response.responseText); |
942 | 154 | request_build_overlay.get("form_submit_button") | 253 | request_build_overlay.get("form_submit_button") |
944 | 155 | .setStyle('display', 'none'); | 254 | .addClass('unseen'); |
945 | 156 | request_build_overlay.renderUI(); | 255 | request_build_overlay.renderUI(); |
946 | 157 | //We want to force the form to be re-created | 256 | //We want to force the form to be re-created |
947 | 158 | request_build_overlay = null; | 257 | request_build_overlay = null; |
948 | @@ -164,19 +263,11 @@ | |||
949 | 164 | errors.push(error_info[field_name]); | 263 | errors.push(error_info[field_name]); |
950 | 165 | handler.showError(errors); | 264 | handler.showError(errors); |
951 | 166 | }), | 265 | }), |
953 | 167 | success: response_handler.getSuccessHandler( | 266 | success: request_build_response_handler.getSuccessHandler( |
954 | 168 | function(handler, id, response) { | 267 | function(handler, id, response) { |
955 | 169 | request_build_overlay.hide(); | 268 | request_build_overlay.hide(); |
966 | 170 | var target = Y.one('#builds-target'); | 269 | display_build_records( |
967 | 171 | target.set('innerHTML', response.responseText); | 270 | response.responseText, current_builds) |
958 | 172 | var new_builds = harvest_current_build_records(); | ||
959 | 173 | Y.Array.each(new_builds, function(row_id) { | ||
960 | 174 | if (current_builds.indexOf(row_id)>=0) | ||
961 | 175 | return; | ||
962 | 176 | var row = Y.one('#'+row_id); | ||
963 | 177 | var anim = Y.lazr.anim.green_flash({node: row}); | ||
964 | 178 | anim.run(); | ||
965 | 179 | }); | ||
968 | 180 | }) | 271 | }) |
969 | 181 | }, | 272 | }, |
970 | 182 | form: { | 273 | form: { |
971 | @@ -190,14 +281,14 @@ | |||
972 | 190 | /* | 281 | /* |
973 | 191 | * Show the temporary "Requesting..." text | 282 | * Show the temporary "Requesting..." text |
974 | 192 | */ | 283 | */ |
976 | 193 | function create_temporary_spinner() { | 284 | function create_temporary_spinner(text, node) { |
977 | 194 | // Add the temp "Requesting build..." text | 285 | // Add the temp "Requesting build..." text |
978 | 195 | var temp_spinner = Y.Node.create([ | 286 | var temp_spinner = Y.Node.create([ |
979 | 196 | '<div id="temp-spinner">', | 287 | '<div id="temp-spinner">', |
981 | 197 | '<img src="/@@/spinner"/>Requesting build...', | 288 | '<img src="/@@/spinner"/>', |
982 | 289 | text, | ||
983 | 198 | '</div>'].join('')); | 290 | '</div>'].join('')); |
986 | 199 | var request_build_handle = Y.one('.yui3-lazr-formoverlay-actions'); | 291 | node.insert(temp_spinner, node); |
985 | 200 | request_build_handle.insert(temp_spinner, request_build_handle); | ||
987 | 201 | } | 292 | } |
988 | 202 | 293 | ||
989 | 203 | /* | 294 | /* |
990 | 204 | 295 | ||
991 | === modified file 'lib/lp/code/model/sourcepackagerecipe.py' | |||
992 | --- lib/lp/code/model/sourcepackagerecipe.py 2011-02-18 23:51:57 +0000 | |||
993 | +++ lib/lp/code/model/sourcepackagerecipe.py 2011-02-23 10:25:23 +0000 | |||
994 | @@ -281,6 +281,20 @@ | |||
995 | 281 | queue_record.manualScore(queue_record.lastscore + 100) | 281 | queue_record.manualScore(queue_record.lastscore + 100) |
996 | 282 | return build | 282 | return build |
997 | 283 | 283 | ||
998 | 284 | def performDailyBuild(self): | ||
999 | 285 | """See `ISourcePackageRecipe`.""" | ||
1000 | 286 | builds = [] | ||
1001 | 287 | self.is_stale = False | ||
1002 | 288 | for distroseries in self.distroseries: | ||
1003 | 289 | try: | ||
1004 | 290 | build = self.requestBuild( | ||
1005 | 291 | self.daily_build_archive, self.owner, | ||
1006 | 292 | distroseries, PackagePublishingPocket.RELEASE) | ||
1007 | 293 | builds.append(build) | ||
1008 | 294 | except BuildAlreadyPending: | ||
1009 | 295 | continue | ||
1010 | 296 | return builds | ||
1011 | 297 | |||
1012 | 284 | def getBuilds(self): | 298 | def getBuilds(self): |
1013 | 285 | """See `ISourcePackageRecipe`.""" | 299 | """See `ISourcePackageRecipe`.""" |
1014 | 286 | where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD | 300 | where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD |
1015 | 287 | 301 | ||
1016 | === modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt' | |||
1017 | --- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-02-22 09:07:44 +0000 | |||
1018 | +++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-02-23 10:25:23 +0000 | |||
1019 | @@ -16,6 +16,18 @@ | |||
1020 | 16 | font-family: "UbuntuBeta Mono","Ubuntu Mono",monospace; | 16 | font-family: "UbuntuBeta Mono","Ubuntu Mono",monospace; |
1021 | 17 | margin-top: -15px; | 17 | margin-top: -15px; |
1022 | 18 | } | 18 | } |
1023 | 19 | .build-informational { | ||
1024 | 20 | background: #d4e8ff url('/+icing/blue-fade-to-grey'); | ||
1025 | 21 | border: solid #666; | ||
1026 | 22 | border-width: 1px 2px 2px 1px; | ||
1027 | 23 | color: black; | ||
1028 | 24 | padding: 5px 5px 5px 5px; | ||
1029 | 25 | margin-right: 40px | ||
1030 | 26 | } | ||
1031 | 27 | .build-informational::before { | ||
1032 | 28 | padding-right: 5px; | ||
1033 | 29 | content: url('/@@/info'); | ||
1034 | 30 | } | ||
1035 | 19 | </style> | 31 | </style> |
1036 | 20 | </metal:block> | 32 | </metal:block> |
1037 | 21 | 33 | ||
1038 | @@ -60,6 +72,23 @@ | |||
1039 | 60 | </a> | 72 | </a> |
1040 | 61 | </dt> | 73 | </dt> |
1041 | 62 | <dd tal:content="structure view/daily_build_widget"/> | 74 | <dd tal:content="structure view/daily_build_widget"/> |
1042 | 75 | <dd | ||
1043 | 76 | tal:define="link context/menu:context/request_daily_build" | ||
1044 | 77 | tal:condition="link/enabled" | ||
1045 | 78 | > | ||
1046 | 79 | <noscript> | ||
1047 | 80 | <form action="+request-daily-build" | ||
1048 | 81 | method="post" | ||
1049 | 82 | id="request-daily-build-form"> | ||
1050 | 83 | <input type="submit" name="field.actions.build" | ||
1051 | 84 | id="field.actions.build" value="Build now" /> | ||
1052 | 85 | </form> | ||
1053 | 86 | </noscript> | ||
1054 | 87 | <a id="request-daily-build" | ||
1055 | 88 | class="sprite source-package-recipe js-action unseen" | ||
1056 | 89 | tal:attributes="href link/url" | ||
1057 | 90 | tal:content="link/text"/> | ||
1058 | 91 | </dd> | ||
1059 | 63 | </dl> | 92 | </dl> |
1060 | 64 | 93 | ||
1061 | 65 | <dl id="owner"> | 94 | <dl id="owner"> |
1062 | @@ -116,11 +145,16 @@ | |||
1063 | 116 | if(Y.UA.ie) { | 145 | if(Y.UA.ie) { |
1064 | 117 | return; | 146 | return; |
1065 | 118 | } | 147 | } |
1066 | 119 | |||
1067 | 120 | Y.on('load', function() { | 148 | Y.on('load', function() { |
1068 | 121 | var logged_in = LP.client.links['me'] !== undefined; | 149 | var logged_in = LP.client.links['me'] !== undefined; |
1069 | 122 | if (logged_in) { | 150 | if (logged_in) { |
1071 | 123 | Y.lp.code.requestbuild_overlay.connect_requestbuild(); | 151 | build_now_link = Y.one('#request-daily-build'); |
1072 | 152 | if( build_now_link != null ) { | ||
1073 | 153 | build_now_link.removeClass('unseen'); | ||
1074 | 154 | Y.lp.code.requestbuild_overlay.connect_requestdailybuild(); | ||
1075 | 155 | } | ||
1076 | 156 | Y.lp.code.requestbuild_overlay.connect_requestbuilds(); | ||
1077 | 157 | |||
1078 | 124 | } | 158 | } |
1079 | 125 | }, window); | 159 | }, window); |
1080 | 126 | });" | 160 | });" |
1081 | 127 | 161 | ||
1082 | === modified file 'lib/lp/code/windmill/tests/test_recipe_request_build.py' | |||
1083 | --- lib/lp/code/windmill/tests/test_recipe_request_build.py 2011-02-18 01:06:19 +0000 | |||
1084 | +++ lib/lp/code/windmill/tests/test_recipe_request_build.py 2011-02-23 10:25:23 +0000 | |||
1085 | @@ -13,13 +13,12 @@ | |||
1086 | 13 | from lp.testing.windmill.constants import ( | 13 | from lp.testing.windmill.constants import ( |
1087 | 14 | FOR_ELEMENT, | 14 | FOR_ELEMENT, |
1088 | 15 | PAGE_LOAD, | 15 | PAGE_LOAD, |
1089 | 16 | SLEEP, | ||
1090 | 17 | ) | 16 | ) |
1091 | 18 | from lp.testing.windmill.lpuser import login_person | 17 | from lp.testing.windmill.lpuser import login_person |
1092 | 19 | from lp.app.browser.tales import PPAFormatterAPI | 18 | from lp.app.browser.tales import PPAFormatterAPI |
1093 | 20 | from lp.code.windmill.testing import CodeWindmillLayer | 19 | from lp.code.windmill.testing import CodeWindmillLayer |
1094 | 21 | from lp.soyuz.model.processor import ProcessorFamily | 20 | from lp.soyuz.model.processor import ProcessorFamily |
1096 | 22 | from lp.testing import WindmillTestCase | 21 | from lp.testing import WindmillTestCase, quote_jquery_expression |
1097 | 23 | 22 | ||
1098 | 24 | 23 | ||
1099 | 25 | class TestRecipeBuild(WindmillTestCase): | 24 | class TestRecipeBuild(WindmillTestCase): |
1100 | @@ -50,7 +49,7 @@ | |||
1101 | 50 | owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe', | 49 | owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe', |
1102 | 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' |
1103 | 52 | ' Secret Squirrel changes.', branches=[cake_branch], | 51 | ' Secret Squirrel changes.', branches=[cake_branch], |
1105 | 53 | daily_build_archive=self.ppa) | 52 | daily_build_archive=self.ppa, build_daily=True, is_stale=True) |
1106 | 54 | transaction.commit() | 53 | transaction.commit() |
1107 | 55 | login_person(self.chef, "chef@example.com", "test", self.client) | 54 | login_person(self.chef, "chef@example.com", "test", self.client) |
1108 | 56 | 55 | ||
1109 | @@ -74,8 +73,8 @@ | |||
1110 | 74 | 73 | ||
1111 | 75 | # Ensure it shows up. | 74 | # Ensure it shows up. |
1112 | 76 | client.waits.forElement( | 75 | client.waits.forElement( |
1115 | 77 | xpath = (u'//tr[contains(@class, "package-build")]/td[4]' | 76 | jquery=u"('tr.package-build a[href$=\"%s\"]')" |
1116 | 78 | '/a[@href="%s"]') % PPAFormatterAPI(self.ppa).url(), | 77 | % quote_jquery_expression(PPAFormatterAPI(self.ppa).url()), |
1117 | 79 | timeout=FOR_ELEMENT) | 78 | timeout=FOR_ELEMENT) |
1118 | 80 | 79 | ||
1119 | 81 | # And try the same one again. | 80 | # And try the same one again. |
1120 | @@ -85,12 +84,27 @@ | |||
1121 | 85 | 84 | ||
1122 | 86 | # And check that there's an error. | 85 | # And check that there's an error. |
1123 | 87 | client.waits.forElement( | 86 | client.waits.forElement( |
1131 | 88 | xpath = ( | 87 | jquery=u"('div.yui3-lazr-formoverlay-errors ul li')", |
1132 | 89 | u'//div[contains(@class, "yui3-lazr-formoverlay-errors")]' | 88 | timeout=FOR_ELEMENT) |
1133 | 90 | '/ul/li'), timeout=FOR_ELEMENT) | 89 | |
1134 | 91 | client.asserts.assertText( | 90 | client.asserts.assertTextIn( |
1135 | 92 | xpath = ( | 91 | jquery=u"('div.yui3-lazr-formoverlay-errors ul li')[0]", |
1129 | 93 | u'//div[contains(@class, "yui3-lazr-formoverlay-errors")]' | ||
1130 | 94 | '/ul/li'), | ||
1136 | 95 | validator=u'An identical build is already pending for %s %s.' | 92 | validator=u'An identical build is already pending for %s %s.' |
1137 | 96 | % (self.ppa.distribution.name, self.squirrel.name)) | 93 | % (self.ppa.distribution.name, self.squirrel.name)) |
1138 | 94 | |||
1139 | 95 | def test_recipe_daily_build_request(self): | ||
1140 | 96 | """Request a recipe build.""" | ||
1141 | 97 | |||
1142 | 98 | client = self.client | ||
1143 | 99 | client.open(url=canonical_url(self.recipe)) | ||
1144 | 100 | client.waits.forElement( | ||
1145 | 101 | id=u'request-daily-build', timeout=PAGE_LOAD) | ||
1146 | 102 | |||
1147 | 103 | # Request a daily build. | ||
1148 | 104 | client.click(id=u'request-daily-build') | ||
1149 | 105 | |||
1150 | 106 | # Ensure it shows up. | ||
1151 | 107 | client.waits.forElement( | ||
1152 | 108 | jquery=u"('tr.package-build a[href$=\"%s\"]')" | ||
1153 | 109 | % quote_jquery_expression(PPAFormatterAPI(self.ppa).url()), | ||
1154 | 110 | timeout=FOR_ELEMENT) | ||
1155 | 97 | 111 | ||
1156 | === modified file 'lib/lp/testing/__init__.py' | |||
1157 | --- lib/lp/testing/__init__.py 2011-02-11 00:25:51 +0000 | |||
1158 | +++ lib/lp/testing/__init__.py 2011-02-23 10:25:23 +0000 | |||
1159 | @@ -28,6 +28,7 @@ | |||
1160 | 28 | 'normalize_whitespace', | 28 | 'normalize_whitespace', |
1161 | 29 | 'oauth_access_token_for', | 29 | 'oauth_access_token_for', |
1162 | 30 | 'person_logged_in', | 30 | 'person_logged_in', |
1163 | 31 | 'quote_jquery_expression' | ||
1164 | 31 | 'record_statements', | 32 | 'record_statements', |
1165 | 32 | 'run_with_login', | 33 | 'run_with_login', |
1166 | 33 | 'run_with_storm_debug', | 34 | 'run_with_storm_debug', |
1167 | @@ -790,6 +791,12 @@ | |||
1168 | 790 | return client, obj_url | 791 | return client, obj_url |
1169 | 791 | 792 | ||
1170 | 792 | 793 | ||
1171 | 794 | def quote_jquery_expression(expression): | ||
1172 | 795 | """jquery requires meta chars used in literals escaped with \\""" | ||
1173 | 796 | return re.sub( | ||
1174 | 797 | "([#!$%&()+,./:;?@~|^{}\\[\\]`*\\\'\\\"])", r"\\\\\1", expression) | ||
1175 | 798 | |||
1176 | 799 | |||
1177 | 793 | class YUIUnitTestCase(WindmillTestCase): | 800 | class YUIUnitTestCase(WindmillTestCase): |
1178 | 794 | 801 | ||
1179 | 795 | layer = None | 802 | layer = None |
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