Merge lp:~thumper/launchpad/recipe-new-ppa into lp:launchpad
- recipe-new-ppa
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Tim Penhey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12074 |
Proposed branch: | lp:~thumper/launchpad/recipe-new-ppa |
Merge into: | lp:launchpad |
Diff against target: |
742 lines (+425/-51) 11 files modified
lib/canonical/launchpad/testing/pages.py (+18/-8) lib/lp/app/browser/launchpadform.py (+20/-0) lib/lp/app/templates/base-layout-macros.pt (+3/-0) lib/lp/code/browser/sourcepackagerecipe.py (+86/-10) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+128/-0) lib/lp/code/javascript/sourcepackagerecipe.new.js (+56/-0) lib/lp/code/model/sourcepackagerecipe.py (+4/-1) lib/lp/code/templates/sourcepackagerecipe-new.pt (+85/-2) lib/lp/registry/browser/productseries.py (+12/-23) lib/lp/soyuz/model/archive.py (+1/-1) lib/lp/testing/__init__.py (+12/-6) |
To merge this branch: | bzr merge lp:~thumper/launchpad/recipe-new-ppa |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Henning Eggers (community) | ui | Approve | |
Graham Binns (community) | code | Approve | |
Review via email: mp+42805@code.launchpad.net |
Commit message
[r=gmb]
Description of the change
The main purpose of this branch is to offer the user the ability to create a
PPA while they are creating a new recipe. Before this work a simple select
control was shown with the possible PPAs in it. If the user did not have any
PPAs, nor was in any team that had a PPA, this select control was empty (and
required).
Now if the user does not have any possible PPAs to choose, they are just shown
an entry field for the new PPA name. The name is optional, and 'ppa' is used
if nothing is entered (this is the default used by Person.createPPA).
If the use does have one or more possible PPAs, there are now radio buttons
shown to the user to indicate whether to use an existing PPA, or to create a
new one. If the user has javascript enabled, the appropriate fields are
enabled or disabled based on the radio button choices.
And now for the changes...
lib/canonical/
- generalises the print_radio_
lib/lp/
- Moves the render_
lib/lp/
- Add the new javascript file
lib/lp/
- Split the ISourcePackageA
- Add two more fields to the Add schema for the ppa options and name
lib/lp/
- the tests for the new recipe work
lib/lp/
- the javascript file that enables and disables the fields.
lib/lp/
- Add ubuntu to the supported_distros for the builable distroseries
This was needed to make the distroseries appear on the new recipe
page when the user didn't yet have any PPAs
lib/lp/
- The form went from being a simple widget rendering using the defaults,
to one where we need to specify the widgets in order to control the
optionality of some of the fields.
lib/lp/
- Move the render method somewhere reusable.
lib/lp/
- drive by lint fix
lib/lp/
- generalise the getting the main content
Henning Eggers (henninge) wrote : | # |
Hi Tim,
great idea to fix this! I just wish we had a nice "combo box" widget that would allow selection or creation. I don't know if the picker widget (branch/person) could be used for that, too. But your solution is nice, too.
I have one major complaint, though: It does not work. When I select "Create new PPA for this recipe" and submit the form, I get an error "Required input is missing." under the drop-down box and if I left the text field empty it complains that "You already have a PPA named 'ppa'." The latter is actually true but confusing because the text field is empty.
It is true that person/
Sorry but this cannot yet be landed without these issues being resolved.
Cheers, Henning
Tim Penhey (thumper) wrote : | # |
> I have one major complaint, though: It does not work.
Oops, must have not tested that bit.
I've updated it to make sure that the ppa name must be entered if a new ppa is being created, also default the ppa name to 'ppa' if it is the first ppa.
Updated the code also with Graham's suggestions.
Henning Eggers (henninge) wrote : | # |
Thanks for the fix! This looks good now UI-wise.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/testing/pages.py' | |||
2 | --- lib/canonical/launchpad/testing/pages.py 2010-11-08 12:52:43 +0000 | |||
3 | +++ lib/canonical/launchpad/testing/pages.py 2010-12-14 20:58:56 +0000 | |||
4 | @@ -319,27 +319,37 @@ | |||
5 | 319 | print sep.join(row_content) | 319 | print sep.join(row_content) |
6 | 320 | 320 | ||
7 | 321 | 321 | ||
10 | 322 | def print_radio_button_field(content, name): | 322 | def get_radio_button_text_for_field(soup, name): |
11 | 323 | """Find the input called field.name, and print a friendly representation. | 323 | """Find the input called field.name, and return an iterable of strings. |
12 | 324 | 324 | ||
13 | 325 | The resulting output will look something like: | 325 | The resulting output will look something like: |
16 | 326 | (*) A checked option | 326 | ['(*) A checked option', '( ) An unchecked option'] |
15 | 327 | ( ) An unchecked option | ||
17 | 328 | """ | 327 | """ |
20 | 329 | main = BeautifulSoup(content) | 328 | buttons = soup.findAll( |
19 | 330 | buttons = main.findAll( | ||
21 | 331 | 'input', {'name': 'field.%s' % name}) | 329 | 'input', {'name': 'field.%s' % name}) |
22 | 332 | for button in buttons: | 330 | for button in buttons: |
23 | 333 | if button.parent.name == 'label': | 331 | if button.parent.name == 'label': |
24 | 334 | label = extract_text(button.parent) | 332 | label = extract_text(button.parent) |
25 | 335 | else: | 333 | else: |
26 | 336 | label = extract_text( | 334 | label = extract_text( |
28 | 337 | main.find('label', attrs={'for': button['id']})) | 335 | soup.find('label', attrs={'for': button['id']})) |
29 | 338 | if button.get('checked', None): | 336 | if button.get('checked', None): |
30 | 339 | radio = '(*)' | 337 | radio = '(*)' |
31 | 340 | else: | 338 | else: |
32 | 341 | radio = '( )' | 339 | radio = '( )' |
34 | 342 | print radio, label | 340 | yield "%s %s" % (radio, label) |
35 | 341 | |||
36 | 342 | |||
37 | 343 | def print_radio_button_field(content, name): | ||
38 | 344 | """Find the input called field.name, and print a friendly representation. | ||
39 | 345 | |||
40 | 346 | The resulting output will look something like: | ||
41 | 347 | (*) A checked option | ||
42 | 348 | ( ) An unchecked option | ||
43 | 349 | """ | ||
44 | 350 | main = BeautifulSoup(content) | ||
45 | 351 | for field in get_radio_button_text_for_field(main, name): | ||
46 | 352 | print field | ||
47 | 343 | 353 | ||
48 | 344 | 354 | ||
49 | 345 | def strip_label(label): | 355 | def strip_label(label): |
50 | 346 | 356 | ||
51 | === modified file 'lib/lp/app/browser/launchpadform.py' | |||
52 | --- lib/lp/app/browser/launchpadform.py 2010-11-24 03:35:12 +0000 | |||
53 | +++ lib/lp/app/browser/launchpadform.py 2010-12-14 20:58:56 +0000 | |||
54 | @@ -12,6 +12,7 @@ | |||
55 | 12 | 'has_structured_doc', | 12 | 'has_structured_doc', |
56 | 13 | 'LaunchpadEditFormView', | 13 | 'LaunchpadEditFormView', |
57 | 14 | 'LaunchpadFormView', | 14 | 'LaunchpadFormView', |
58 | 15 | 'render_radio_widget_part', | ||
59 | 15 | 'ReturnToReferrerMixin', | 16 | 'ReturnToReferrerMixin', |
60 | 16 | 'safe_action', | 17 | 'safe_action', |
61 | 17 | ] | 18 | ] |
62 | @@ -543,3 +544,22 @@ | |||
63 | 543 | "There should be no further path segments after " | 544 | "There should be no further path segments after " |
64 | 544 | "query:has-structured-doc") | 545 | "query:has-structured-doc") |
65 | 545 | return self.widget.context.queryTaggedValue('has_structured_doc') | 546 | return self.widget.context.queryTaggedValue('has_structured_doc') |
66 | 547 | |||
67 | 548 | |||
68 | 549 | def render_radio_widget_part(widget, term_value, current_value, label=None): | ||
69 | 550 | """Render a particular term for a radio button widget. | ||
70 | 551 | |||
71 | 552 | This may well work for other widgets, but has only been tested with radio | ||
72 | 553 | button widgets. | ||
73 | 554 | """ | ||
74 | 555 | term = widget.vocabulary.getTerm(term_value) | ||
75 | 556 | if term.value == current_value: | ||
76 | 557 | render = widget.renderSelectedItem | ||
77 | 558 | else: | ||
78 | 559 | render = widget.renderItem | ||
79 | 560 | if label is None: | ||
80 | 561 | label = term.title | ||
81 | 562 | value = term.token | ||
82 | 563 | return render( | ||
83 | 564 | index=term.value, text=label, value=value, name=widget.name, | ||
84 | 565 | cssClass='') | ||
85 | 546 | 566 | ||
86 | === modified file 'lib/lp/app/templates/base-layout-macros.pt' | |||
87 | --- lib/lp/app/templates/base-layout-macros.pt 2010-12-13 18:04:24 +0000 | |||
88 | +++ lib/lp/app/templates/base-layout-macros.pt 2010-12-14 20:58:56 +0000 | |||
89 | @@ -607,6 +607,9 @@ | |||
90 | 607 | tal:attributes="src string:${lp_js}/code/productseries-setbranch.js"> | 607 | tal:attributes="src string:${lp_js}/code/productseries-setbranch.js"> |
91 | 608 | </script> | 608 | </script> |
92 | 609 | <script type="text/javascript" | 609 | <script type="text/javascript" |
93 | 610 | tal:attributes="src string:${lp_js}/code/sourcepackagerecipe.new.js"> | ||
94 | 611 | </script> | ||
95 | 612 | <script type="text/javascript" | ||
96 | 610 | tal:attributes="src string:${lp_js}/app/comment.js"></script> | 613 | tal:attributes="src string:${lp_js}/app/comment.js"></script> |
97 | 611 | <script type="text/javascript" | 614 | <script type="text/javascript" |
98 | 612 | tal:attributes="src string:${lp_js}/app/errors.js"></script> | 615 | tal:attributes="src string:${lp_js}/app/errors.js"></script> |
99 | 613 | 616 | ||
100 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' | |||
101 | --- lib/lp/code/browser/sourcepackagerecipe.py 2010-11-28 23:32:25 +0000 | |||
102 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2010-12-14 20:58:56 +0000 | |||
103 | @@ -36,10 +36,17 @@ | |||
104 | 36 | Choice, | 36 | Choice, |
105 | 37 | List, | 37 | List, |
106 | 38 | Text, | 38 | Text, |
107 | 39 | TextLine, | ||
108 | 40 | ) | ||
109 | 41 | from zope.schema.vocabulary import ( | ||
110 | 42 | SimpleTerm, | ||
111 | 43 | SimpleVocabulary, | ||
112 | 39 | ) | 44 | ) |
113 | 40 | 45 | ||
114 | 41 | from canonical.database.constants import UTC_NOW | 46 | from canonical.database.constants import UTC_NOW |
115 | 47 | from canonical.launchpad import _ | ||
116 | 42 | from canonical.launchpad.browser.launchpad import Hierarchy | 48 | from canonical.launchpad.browser.launchpad import Hierarchy |
117 | 49 | from canonical.launchpad.validators.name import name_validator | ||
118 | 43 | from canonical.launchpad.webapp import ( | 50 | from canonical.launchpad.webapp import ( |
119 | 44 | canonical_url, | 51 | canonical_url, |
120 | 45 | ContextMenu, | 52 | ContextMenu, |
121 | @@ -54,13 +61,17 @@ | |||
122 | 54 | from canonical.launchpad.webapp.authorization import check_permission | 61 | from canonical.launchpad.webapp.authorization import check_permission |
123 | 55 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 62 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
124 | 56 | from canonical.widgets.suggestion import RecipeOwnerWidget | 63 | from canonical.widgets.suggestion import RecipeOwnerWidget |
126 | 57 | from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget | 64 | from canonical.widgets.itemswidgets import ( |
127 | 65 | LabeledMultiCheckBoxWidget, | ||
128 | 66 | LaunchpadRadioWidget, | ||
129 | 67 | ) | ||
130 | 58 | from lp.app.browser.launchpadform import ( | 68 | from lp.app.browser.launchpadform import ( |
131 | 59 | action, | 69 | action, |
132 | 60 | custom_widget, | 70 | custom_widget, |
133 | 61 | has_structured_doc, | 71 | has_structured_doc, |
134 | 62 | LaunchpadEditFormView, | 72 | LaunchpadEditFormView, |
135 | 63 | LaunchpadFormView, | 73 | LaunchpadFormView, |
136 | 74 | render_radio_widget_part, | ||
137 | 64 | ) | 75 | ) |
138 | 65 | from lp.code.errors import ( | 76 | from lp.code.errors import ( |
139 | 66 | BuildAlreadyPending, | 77 | BuildAlreadyPending, |
140 | @@ -77,6 +88,7 @@ | |||
141 | 77 | ISourcePackageRecipeBuildSource, | 88 | ISourcePackageRecipeBuildSource, |
142 | 78 | ) | 89 | ) |
143 | 79 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 90 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
144 | 91 | from lp.soyuz.model.archive import Archive | ||
145 | 80 | 92 | ||
146 | 81 | 93 | ||
147 | 82 | RECIPE_BETA_MESSAGE = structured( | 94 | RECIPE_BETA_MESSAGE = structured( |
148 | @@ -268,7 +280,7 @@ | |||
149 | 268 | self.next_url = self.cancel_url | 280 | self.next_url = self.cancel_url |
150 | 269 | 281 | ||
151 | 270 | 282 | ||
153 | 271 | class ISourcePackageAddEditSchema(Interface): | 283 | class ISourcePackageEditSchema(Interface): |
154 | 272 | """Schema for adding or editing a recipe.""" | 284 | """Schema for adding or editing a recipe.""" |
155 | 273 | 285 | ||
156 | 274 | use_template(ISourcePackageRecipe, include=[ | 286 | use_template(ISourcePackageRecipe, include=[ |
157 | @@ -300,6 +312,38 @@ | |||
158 | 300 | """)) | 312 | """)) |
159 | 301 | 313 | ||
160 | 302 | 314 | ||
161 | 315 | EXISTING_PPA = 'existing-ppa' | ||
162 | 316 | CREATE_NEW = 'create-new' | ||
163 | 317 | |||
164 | 318 | |||
165 | 319 | USE_ARCHIVE_VOCABULARY = SimpleVocabulary(( | ||
166 | 320 | SimpleTerm(EXISTING_PPA, EXISTING_PPA, _("Use an existing PPA")), | ||
167 | 321 | SimpleTerm( | ||
168 | 322 | CREATE_NEW, CREATE_NEW, _("Create a new PPA for this recipe")), | ||
169 | 323 | )) | ||
170 | 324 | |||
171 | 325 | |||
172 | 326 | class ISourcePackageAddSchema(ISourcePackageEditSchema): | ||
173 | 327 | |||
174 | 328 | daily_build_archive = Choice(vocabulary='TargetPPAs', | ||
175 | 329 | title=u'Daily build archive', required=False, | ||
176 | 330 | description=( | ||
177 | 331 | u'If built daily, this is the archive where the package ' | ||
178 | 332 | u'will be uploaded.')) | ||
179 | 333 | |||
180 | 334 | use_ppa = Choice( | ||
181 | 335 | title=_('Which PPA'), | ||
182 | 336 | vocabulary=USE_ARCHIVE_VOCABULARY, | ||
183 | 337 | description=_("Which PPA to use..."), | ||
184 | 338 | required=True) | ||
185 | 339 | |||
186 | 340 | ppa_name = TextLine( | ||
187 | 341 | title=_("New PPA name"), required=False, | ||
188 | 342 | constraint=name_validator, | ||
189 | 343 | description=_("A new PPA with this name will be created for " | ||
190 | 344 | "the owner of the recipe .")) | ||
191 | 345 | |||
192 | 346 | |||
193 | 303 | class RecipeTextValidatorMixin: | 347 | class RecipeTextValidatorMixin: |
194 | 304 | """Class to validate that the Source Package Recipe text is valid.""" | 348 | """Class to validate that the Source Package Recipe text is valid.""" |
195 | 305 | 349 | ||
196 | @@ -321,22 +365,39 @@ | |||
197 | 321 | 365 | ||
198 | 322 | title = label = 'Create a new source package recipe' | 366 | title = label = 'Create a new source package recipe' |
199 | 323 | 367 | ||
201 | 324 | schema = ISourcePackageAddEditSchema | 368 | schema = ISourcePackageAddSchema |
202 | 325 | custom_widget('distros', LabeledMultiCheckBoxWidget) | 369 | custom_widget('distros', LabeledMultiCheckBoxWidget) |
203 | 326 | custom_widget('owner', RecipeOwnerWidget) | 370 | custom_widget('owner', RecipeOwnerWidget) |
204 | 371 | custom_widget('use_ppa', LaunchpadRadioWidget) | ||
205 | 327 | 372 | ||
206 | 328 | def initialize(self): | 373 | def initialize(self): |
207 | 329 | # XXX: rockstar: This should be removed when source package recipes | ||
208 | 330 | # are put into production. spec=sourcepackagerecipes | ||
209 | 331 | super(SourcePackageRecipeAddView, self).initialize() | 374 | super(SourcePackageRecipeAddView, self).initialize() |
210 | 375 | # XXX: rockstar: This should be removed when source package recipes | ||
211 | 376 | # are put into production. spec=sourcepackagerecipes | ||
212 | 332 | self.request.response.addWarningNotification(RECIPE_BETA_MESSAGE) | 377 | self.request.response.addWarningNotification(RECIPE_BETA_MESSAGE) |
213 | 378 | widget = self.widgets['use_ppa'] | ||
214 | 379 | current_value = widget._getFormValue() | ||
215 | 380 | self.use_ppa_existing = render_radio_widget_part( | ||
216 | 381 | widget, EXISTING_PPA, current_value) | ||
217 | 382 | self.use_ppa_new = render_radio_widget_part( | ||
218 | 383 | widget, CREATE_NEW, current_value) | ||
219 | 384 | archive_widget = self.widgets['daily_build_archive'] | ||
220 | 385 | self.show_ppa_chooser = len(archive_widget.vocabulary) > 0 | ||
221 | 386 | if not self.show_ppa_chooser: | ||
222 | 387 | self.widgets['ppa_name'].setRenderedValue('ppa') | ||
223 | 388 | # Force there to be no '(no value)' item in the select. We do this as | ||
224 | 389 | # the input isn't listed as 'required' otherwise the validator gets | ||
225 | 390 | # all confused when we want to create a new PPA. | ||
226 | 391 | archive_widget._displayItemForMissingValue = False | ||
227 | 333 | 392 | ||
228 | 334 | @property | 393 | @property |
229 | 335 | def initial_values(self): | 394 | def initial_values(self): |
230 | 336 | return { | 395 | return { |
231 | 337 | 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity, | 396 | 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity, |
232 | 338 | 'owner': self.user, | 397 | 'owner': self.user, |
234 | 339 | 'build_daily': False} | 398 | 'build_daily': False, |
235 | 399 | 'use_ppa': EXISTING_PPA, | ||
236 | 400 | } | ||
237 | 340 | 401 | ||
238 | 341 | @property | 402 | @property |
239 | 342 | def cancel_url(self): | 403 | def cancel_url(self): |
240 | @@ -345,11 +406,17 @@ | |||
241 | 345 | @action('Create Recipe', name='create') | 406 | @action('Create Recipe', name='create') |
242 | 346 | def request_action(self, action, data): | 407 | def request_action(self, action, data): |
243 | 347 | try: | 408 | try: |
244 | 409 | owner = data['owner'] | ||
245 | 410 | if data['use_ppa'] == CREATE_NEW: | ||
246 | 411 | ppa_name = data.get('ppa_name', None) | ||
247 | 412 | ppa = owner.createPPA(ppa_name) | ||
248 | 413 | else: | ||
249 | 414 | ppa = data['daily_build_archive'] | ||
250 | 348 | source_package_recipe = getUtility( | 415 | source_package_recipe = getUtility( |
251 | 349 | ISourcePackageRecipeSource).new( | 416 | ISourcePackageRecipeSource).new( |
253 | 350 | self.user, data['owner'], data['name'], | 417 | self.user, owner, data['name'], |
254 | 351 | data['recipe_text'], data['description'], data['distros'], | 418 | data['recipe_text'], data['description'], data['distros'], |
256 | 352 | data['daily_build_archive'], data['build_daily']) | 419 | ppa, data['build_daily']) |
257 | 353 | Store.of(source_package_recipe).flush() | 420 | Store.of(source_package_recipe).flush() |
258 | 354 | except TooNewRecipeFormat: | 421 | except TooNewRecipeFormat: |
259 | 355 | self.setFieldError( | 422 | self.setFieldError( |
260 | @@ -383,6 +450,15 @@ | |||
261 | 383 | 'name', | 450 | 'name', |
262 | 384 | 'There is already a recipe owned by %s with this name.' % | 451 | 'There is already a recipe owned by %s with this name.' % |
263 | 385 | owner.displayname) | 452 | owner.displayname) |
264 | 453 | if data['use_ppa'] == CREATE_NEW: | ||
265 | 454 | ppa_name = data.get('ppa_name', None) | ||
266 | 455 | if ppa_name is None: | ||
267 | 456 | self.setFieldError( | ||
268 | 457 | 'ppa_name', 'You need to specify a name for the PPA.') | ||
269 | 458 | else: | ||
270 | 459 | error = Archive.validatePPA(owner, ppa_name) | ||
271 | 460 | if error is not None: | ||
272 | 461 | self.setFieldError('ppa_name', error) | ||
273 | 386 | 462 | ||
274 | 387 | 463 | ||
275 | 388 | class SourcePackageRecipeEditView(RecipeTextValidatorMixin, | 464 | class SourcePackageRecipeEditView(RecipeTextValidatorMixin, |
276 | @@ -394,7 +470,7 @@ | |||
277 | 394 | return 'Edit %s source package recipe' % self.context.name | 470 | return 'Edit %s source package recipe' % self.context.name |
278 | 395 | label = title | 471 | label = title |
279 | 396 | 472 | ||
281 | 397 | schema = ISourcePackageAddEditSchema | 473 | schema = ISourcePackageEditSchema |
282 | 398 | custom_widget('distros', LabeledMultiCheckBoxWidget) | 474 | custom_widget('distros', LabeledMultiCheckBoxWidget) |
283 | 399 | 475 | ||
284 | 400 | def setUpFields(self): | 476 | def setUpFields(self): |
285 | @@ -481,7 +557,7 @@ | |||
286 | 481 | @property | 557 | @property |
287 | 482 | def adapters(self): | 558 | def adapters(self): |
288 | 483 | """See `LaunchpadEditFormView`""" | 559 | """See `LaunchpadEditFormView`""" |
290 | 484 | return {ISourcePackageAddEditSchema: self.context} | 560 | return {ISourcePackageEditSchema: self.context} |
291 | 485 | 561 | ||
292 | 486 | def validate(self, data): | 562 | def validate(self, data): |
293 | 487 | super(SourcePackageRecipeEditView, self).validate(data) | 563 | super(SourcePackageRecipeEditView, self).validate(data) |
294 | 488 | 564 | ||
295 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' | |||
296 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-11-30 11:48:27 +0000 | |||
297 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-12-14 20:58:56 +0000 | |||
298 | @@ -25,6 +25,7 @@ | |||
299 | 25 | extract_text, | 25 | extract_text, |
300 | 26 | find_main_content, | 26 | find_main_content, |
301 | 27 | find_tags_by_class, | 27 | find_tags_by_class, |
302 | 28 | get_radio_button_text_for_field, | ||
303 | 28 | ) | 29 | ) |
304 | 29 | from canonical.launchpad.webapp import canonical_url | 30 | from canonical.launchpad.webapp import canonical_url |
305 | 30 | from canonical.testing.layers import ( | 31 | from canonical.testing.layers import ( |
306 | @@ -41,6 +42,7 @@ | |||
307 | 41 | ) | 42 | ) |
308 | 42 | from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT | 43 | from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT |
309 | 43 | from lp.code.tests.helpers import recipe_parser_newest_version | 44 | from lp.code.tests.helpers import recipe_parser_newest_version |
310 | 45 | from lp.registry.interfaces.person import TeamSubscriptionPolicy | ||
311 | 44 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 46 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
312 | 45 | from lp.services.propertycache import clear_property_cache | 47 | from lp.services.propertycache import clear_property_cache |
313 | 46 | from lp.soyuz.model.processor import ProcessorFamily | 48 | from lp.soyuz.model.processor import ProcessorFamily |
314 | @@ -412,6 +414,132 @@ | |||
315 | 412 | get_message_text(browser, 2), | 414 | get_message_text(browser, 2), |
316 | 413 | 'Recipe may not refer to private branch: %s' % bzr_identity) | 415 | 'Recipe may not refer to private branch: %s' % bzr_identity) |
317 | 414 | 416 | ||
318 | 417 | def test_ppa_selector_not_shown_if_user_has_no_ppas(self): | ||
319 | 418 | # If the user creating a recipe has no existing PPAs, the selector | ||
320 | 419 | # isn't shown, but the field to enter a new PPA name is. | ||
321 | 420 | self.user = self.factory.makePerson(password='test') | ||
322 | 421 | branch = self.factory.makeAnyBranch() | ||
323 | 422 | with person_logged_in(self.user): | ||
324 | 423 | content = self.getMainContent(branch, '+new-recipe') | ||
325 | 424 | ppa_name = content.find(attrs={'id': 'field.ppa_name'}) | ||
326 | 425 | self.assertEqual('input', ppa_name.name) | ||
327 | 426 | self.assertEqual('text', ppa_name['type']) | ||
328 | 427 | # The new ppa name field has an initial value. | ||
329 | 428 | self.assertEqual('ppa', ppa_name['value']) | ||
330 | 429 | ppa_chooser = content.find(attrs={'id': 'field.daily_build_archive'}) | ||
331 | 430 | self.assertIs(None, ppa_chooser) | ||
332 | 431 | # There is a hidden option to say create a new ppa. | ||
333 | 432 | ppa_options = content.find(attrs={'name': 'field.use_ppa'}) | ||
334 | 433 | self.assertEqual('input', ppa_options.name) | ||
335 | 434 | self.assertEqual('hidden', ppa_options['type']) | ||
336 | 435 | self.assertEqual('create-new', ppa_options['value']) | ||
337 | 436 | |||
338 | 437 | def test_ppa_selector_shown_if_user_has_ppas(self): | ||
339 | 438 | # If the user creating a recipe has existing PPAs, the selector is | ||
340 | 439 | # shown, along with radio buttons to decide whether to use an existing | ||
341 | 440 | # ppa or to create a new one. | ||
342 | 441 | branch = self.factory.makeAnyBranch() | ||
343 | 442 | with person_logged_in(self.user): | ||
344 | 443 | content = self.getMainContent(branch, '+new-recipe') | ||
345 | 444 | ppa_name = content.find(attrs={'id': 'field.ppa_name'}) | ||
346 | 445 | self.assertEqual('input', ppa_name.name) | ||
347 | 446 | self.assertEqual('text', ppa_name['type']) | ||
348 | 447 | # The new ppa name field has no initial value. | ||
349 | 448 | self.assertEqual('', ppa_name['value']) | ||
350 | 449 | ppa_chooser = content.find(attrs={'id': 'field.daily_build_archive'}) | ||
351 | 450 | self.assertEqual('select', ppa_chooser.name) | ||
352 | 451 | ppa_options = list( | ||
353 | 452 | get_radio_button_text_for_field(content, 'use_ppa')) | ||
354 | 453 | self.assertEqual( | ||
355 | 454 | ['(*) Use an existing PPA', | ||
356 | 455 | '( ) Create a new PPA for this recipe'''], | ||
357 | 456 | ppa_options) | ||
358 | 457 | |||
359 | 458 | def test_create_new_ppa(self): | ||
360 | 459 | # If the user doesn't have any PPAs, a new once can be created. | ||
361 | 460 | self.user = self.factory.makePerson(name='eric', password='test') | ||
362 | 461 | branch = self.factory.makeAnyBranch() | ||
363 | 462 | |||
364 | 463 | # A new recipe can be created from the branch page. | ||
365 | 464 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) | ||
366 | 465 | browser.getLink('Create packaging recipe').click() | ||
367 | 466 | |||
368 | 467 | browser.getControl(name='field.name').value = 'name' | ||
369 | 468 | browser.getControl('Description').value = 'Make some food!' | ||
370 | 469 | browser.getControl('Secret Squirrel').click() | ||
371 | 470 | browser.getControl('Create Recipe').click() | ||
372 | 471 | |||
373 | 472 | # A new recipe is created in a new PPA. | ||
374 | 473 | self.assertTrue(browser.url.endswith('/~eric/+recipe/name')) | ||
375 | 474 | # Since no PPA name was entered, the default name (ppa) was used. | ||
376 | 475 | login(ANONYMOUS) | ||
377 | 476 | new_ppa = self.user.getPPAByName('ppa') | ||
378 | 477 | self.assertIsNot(None, new_ppa) | ||
379 | 478 | |||
380 | 479 | def test_create_new_ppa_duplicate(self): | ||
381 | 480 | # If a new PPA is being created, and the user already has a ppa of the | ||
382 | 481 | # name specifed an error is shown. | ||
383 | 482 | self.user = self.factory.makePerson(name='eric', password='test') | ||
384 | 483 | # Make a PPA called 'ppa' using the default. | ||
385 | 484 | self.user.createPPA(name='foo') | ||
386 | 485 | branch = self.factory.makeAnyBranch() | ||
387 | 486 | |||
388 | 487 | # A new recipe can be created from the branch page. | ||
389 | 488 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) | ||
390 | 489 | browser.getLink('Create packaging recipe').click() | ||
391 | 490 | browser.getControl(name='field.name').value = 'name' | ||
392 | 491 | browser.getControl('Description').value = 'Make some food!' | ||
393 | 492 | browser.getControl('Secret Squirrel').click() | ||
394 | 493 | browser.getControl('Create a new PPA').click() | ||
395 | 494 | browser.getControl(name='field.ppa_name').value = 'foo' | ||
396 | 495 | browser.getControl('Create Recipe').click() | ||
397 | 496 | self.assertEqual( | ||
398 | 497 | get_message_text(browser, 2), | ||
399 | 498 | "You already have a PPA named 'foo'.") | ||
400 | 499 | |||
401 | 500 | def test_create_new_ppa_missing_name(self): | ||
402 | 501 | # If a new PPA is being created, and the user has not specified a | ||
403 | 502 | # name, an error is shown. | ||
404 | 503 | self.user = self.factory.makePerson(name='eric', password='test') | ||
405 | 504 | branch = self.factory.makeAnyBranch() | ||
406 | 505 | |||
407 | 506 | # A new recipe can be created from the branch page. | ||
408 | 507 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) | ||
409 | 508 | browser.getLink('Create packaging recipe').click() | ||
410 | 509 | browser.getControl(name='field.name').value = 'name' | ||
411 | 510 | browser.getControl('Description').value = 'Make some food!' | ||
412 | 511 | browser.getControl('Secret Squirrel').click() | ||
413 | 512 | browser.getControl(name='field.ppa_name').value = '' | ||
414 | 513 | browser.getControl('Create Recipe').click() | ||
415 | 514 | self.assertEqual( | ||
416 | 515 | get_message_text(browser, 2), | ||
417 | 516 | "You need to specify a name for the PPA.") | ||
418 | 517 | |||
419 | 518 | def test_create_new_ppa_owned_by_recipe_owner(self): | ||
420 | 519 | # The new PPA that is created is owned by the recipe owner. | ||
421 | 520 | self.user = self.factory.makePerson(name='eric', password='test') | ||
422 | 521 | team = self.factory.makeTeam( | ||
423 | 522 | name='vikings', members=[self.user], | ||
424 | 523 | subscription_policy=TeamSubscriptionPolicy.MODERATED) | ||
425 | 524 | branch = self.factory.makeAnyBranch(owner=team) | ||
426 | 525 | |||
427 | 526 | # A new recipe can be created from the branch page. | ||
428 | 527 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) | ||
429 | 528 | browser.getLink('Create packaging recipe').click() | ||
430 | 529 | |||
431 | 530 | browser.getControl(name='field.name').value = 'name' | ||
432 | 531 | browser.getControl('Description').value = 'Make some food!' | ||
433 | 532 | browser.getControl(name='field.owner').value = ['vikings'] | ||
434 | 533 | browser.getControl('Secret Squirrel').click() | ||
435 | 534 | browser.getControl('Create Recipe').click() | ||
436 | 535 | |||
437 | 536 | # A new recipe is created in a new PPA. | ||
438 | 537 | self.assertTrue(browser.url.endswith('/~vikings/+recipe/name')) | ||
439 | 538 | # Since no PPA name was entered, the default name (ppa) was used. | ||
440 | 539 | login(ANONYMOUS) | ||
441 | 540 | new_ppa = team.getPPAByName('ppa') | ||
442 | 541 | self.assertIsNot(None, new_ppa) | ||
443 | 542 | |||
444 | 415 | 543 | ||
445 | 416 | class TestSourcePackageRecipeEditView(TestCaseForRecipe): | 544 | class TestSourcePackageRecipeEditView(TestCaseForRecipe): |
446 | 417 | """Test the editing behaviour of a source package recipe.""" | 545 | """Test the editing behaviour of a source package recipe.""" |
447 | 418 | 546 | ||
448 | === added file 'lib/lp/code/javascript/sourcepackagerecipe.new.js' | |||
449 | --- lib/lp/code/javascript/sourcepackagerecipe.new.js 1970-01-01 00:00:00 +0000 | |||
450 | +++ lib/lp/code/javascript/sourcepackagerecipe.new.js 2010-12-14 20:58:56 +0000 | |||
451 | @@ -0,0 +1,56 @@ | |||
452 | 1 | /* Copyright 2010 Canonical Ltd. This software is licensed under the | ||
453 | 2 | * GNU Affero General Public License version 3 (see the file LICENSE). | ||
454 | 3 | * | ||
455 | 4 | * Control enabling/disabling form elements on the +new-recipe page. | ||
456 | 5 | * | ||
457 | 6 | * @module Y.lp.code.sourcepackagerecipe.new | ||
458 | 7 | * @requires node, DOM | ||
459 | 8 | */ | ||
460 | 9 | YUI.add('lp.code.sourcepackagerecipe.new', function(Y) { | ||
461 | 10 | Y.log('loading lp.code.sourcepackagerecipe.new'); | ||
462 | 11 | var module = Y.namespace('lp.code.sourcepackagerecipe.new'); | ||
463 | 12 | |||
464 | 13 | function getRadioSelectedValue(selector) { | ||
465 | 14 | var tmpValue= false; | ||
466 | 15 | Y.all(selector).each(function(node) { | ||
467 | 16 | if (node.get('checked')) | ||
468 | 17 | tmpValue = node.get('value'); | ||
469 | 18 | }); | ||
470 | 19 | return tmpValue; | ||
471 | 20 | } | ||
472 | 21 | |||
473 | 22 | var PPA_SELECTOR_ID = 'field.daily_build_archive'; | ||
474 | 23 | var PPA_NAME_ID = 'field.ppa_name'; | ||
475 | 24 | var set_field_focus = false; | ||
476 | 25 | |||
477 | 26 | function set_enabled(field_id, is_enabled) { | ||
478 | 27 | var field = Y.DOM.byId(field_id); | ||
479 | 28 | field.disabled = !is_enabled; | ||
480 | 29 | if (is_enabled && set_field_focus) field.focus(); | ||
481 | 30 | } | ||
482 | 31 | |||
483 | 32 | module.onclick_use_ppa = function(e) { | ||
484 | 33 | var value = getRadioSelectedValue('input[name=field.use_ppa]'); | ||
485 | 34 | if (value == 'create-new') { | ||
486 | 35 | set_enabled(PPA_NAME_ID, true); | ||
487 | 36 | set_enabled(PPA_SELECTOR_ID, false); | ||
488 | 37 | } | ||
489 | 38 | else { | ||
490 | 39 | set_enabled(PPA_NAME_ID, false); | ||
491 | 40 | set_enabled(PPA_SELECTOR_ID, true); | ||
492 | 41 | } | ||
493 | 42 | }; | ||
494 | 43 | |||
495 | 44 | module.setup = function() { | ||
496 | 45 | Y.all('input[name=field.use_ppa]').on( | ||
497 | 46 | 'click', module.onclick_use_ppa); | ||
498 | 47 | |||
499 | 48 | // Set the initial state. | ||
500 | 49 | module.onclick_use_ppa(); | ||
501 | 50 | // And from now on, set the focus to the active input field when the | ||
502 | 51 | // radio button is clicked. | ||
503 | 52 | set_field_focus = true; | ||
504 | 53 | }; | ||
505 | 54 | |||
506 | 55 | }, "0.1", {"requires": ["node", "DOM"]} | ||
507 | 56 | ); | ||
508 | 0 | 57 | ||
509 | === modified file 'lib/lp/code/model/sourcepackagerecipe.py' | |||
510 | --- lib/lp/code/model/sourcepackagerecipe.py 2010-12-01 11:26:57 +0000 | |||
511 | +++ lib/lp/code/model/sourcepackagerecipe.py 2010-12-14 20:58:56 +0000 | |||
512 | @@ -29,6 +29,7 @@ | |||
513 | 29 | ) | 29 | ) |
514 | 30 | 30 | ||
515 | 31 | from canonical.database.datetimecol import UtcDateTimeCol | 31 | from canonical.database.datetimecol import UtcDateTimeCol |
516 | 32 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | ||
517 | 32 | from canonical.launchpad.interfaces.lpstorm import ( | 33 | from canonical.launchpad.interfaces.lpstorm import ( |
518 | 33 | IMasterStore, | 34 | IMasterStore, |
519 | 34 | IStore, | 35 | IStore, |
520 | @@ -62,7 +63,9 @@ | |||
521 | 62 | 63 | ||
522 | 63 | def get_buildable_distroseries_set(user): | 64 | def get_buildable_distroseries_set(user): |
523 | 64 | ppas = getUtility(IArchiveSet).getPPAsForUser(user) | 65 | ppas = getUtility(IArchiveSet).getPPAsForUser(user) |
525 | 65 | supported_distros = [ppa.distribution for ppa in ppas] | 66 | supported_distros = set([ppa.distribution for ppa in ppas]) |
526 | 67 | # Now add in Ubuntu. | ||
527 | 68 | supported_distros.add(getUtility(ILaunchpadCelebrities).ubuntu) | ||
528 | 66 | distros = getUtility(IDistroSeriesSet).search() | 69 | distros = getUtility(IDistroSeriesSet).search() |
529 | 67 | 70 | ||
530 | 68 | buildables = [] | 71 | buildables = [] |
531 | 69 | 72 | ||
532 | === modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt' | |||
533 | --- lib/lp/code/templates/sourcepackagerecipe-new.pt 2010-11-25 03:35:05 +0000 | |||
534 | +++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2010-12-14 20:58:56 +0000 | |||
535 | @@ -6,6 +6,21 @@ | |||
536 | 6 | metal:use-macro="view/macro:page/main_only" | 6 | metal:use-macro="view/macro:page/main_only" |
537 | 7 | i18n:domain="launchpad"> | 7 | i18n:domain="launchpad"> |
538 | 8 | <body> | 8 | <body> |
539 | 9 | |||
540 | 10 | <metal:block fill-slot="head_epilogue"> | ||
541 | 11 | <style type="text/css"> | ||
542 | 12 | .root-choice input[type="radio"] { | ||
543 | 13 | margin-left: 0; | ||
544 | 14 | } | ||
545 | 15 | .root-choice label { | ||
546 | 16 | font-weight: bold !important; | ||
547 | 17 | } | ||
548 | 18 | .subordinate { | ||
549 | 19 | margin: 0.5em 0 0.5em 2em; | ||
550 | 20 | } | ||
551 | 21 | </style> | ||
552 | 22 | </metal:block> | ||
553 | 23 | |||
554 | 9 | <div metal:fill-slot="main"> | 24 | <div metal:fill-slot="main"> |
555 | 10 | 25 | ||
556 | 11 | <div> | 26 | <div> |
557 | @@ -23,8 +38,76 @@ | |||
558 | 23 | 38 | ||
559 | 24 | </div> | 39 | </div> |
560 | 25 | 40 | ||
563 | 26 | <div metal:use-macro="context/@@launchpad_form/form" /> | 41 | <div metal:use-macro="context/@@launchpad_form/form"> |
564 | 27 | 42 | ||
565 | 43 | <metal:formbody fill-slot="widgets"> | ||
566 | 44 | |||
567 | 45 | <table class="form"> | ||
568 | 46 | |||
569 | 47 | <tal:widget define="widget nocall:view/widgets/name"> | ||
570 | 48 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
571 | 49 | </tal:widget> | ||
572 | 50 | <tal:widget define="widget nocall:view/widgets/description"> | ||
573 | 51 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
574 | 52 | </tal:widget> | ||
575 | 53 | <tal:widget define="widget nocall:view/widgets/owner"> | ||
576 | 54 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
577 | 55 | </tal:widget> | ||
578 | 56 | <tal:widget define="widget nocall:view/widgets/build_daily"> | ||
579 | 57 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
580 | 58 | </tal:widget> | ||
581 | 59 | |||
582 | 60 | <tal:show-ppa-choice condition="view/show_ppa_chooser"> | ||
583 | 61 | <tr> | ||
584 | 62 | <td class='root-choice'> | ||
585 | 63 | <label tal:replace="structure view/use_ppa_existing"> | ||
586 | 64 | Use existing PPA | ||
587 | 65 | </label> | ||
588 | 66 | <table class="subordinate"> | ||
589 | 67 | <tal:widget define="widget nocall:view/widgets/daily_build_archive"> | ||
590 | 68 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
591 | 69 | </tal:widget> | ||
592 | 70 | </table> | ||
593 | 71 | </td> | ||
594 | 72 | </tr> | ||
595 | 73 | |||
596 | 74 | <tr> | ||
597 | 75 | <td class='root-choice'> | ||
598 | 76 | <label tal:replace="structure view/use_ppa_new"> | ||
599 | 77 | Create new PPA | ||
600 | 78 | </label> | ||
601 | 79 | <table class="subordinate"> | ||
602 | 80 | <tal:widget define="widget nocall:view/widgets/ppa_name"> | ||
603 | 81 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
604 | 82 | </tal:widget> | ||
605 | 83 | </table> | ||
606 | 84 | </td> | ||
607 | 85 | </tr> | ||
608 | 86 | |||
609 | 87 | <script type="text/javascript"> | ||
610 | 88 | LPS.use('lp.code.sourcepackagerecipe.new', function(Y) { | ||
611 | 89 | Y.on('domready', Y.lp.code.sourcepackagerecipe.new.setup); | ||
612 | 90 | }); | ||
613 | 91 | </script> | ||
614 | 92 | </tal:show-ppa-choice> | ||
615 | 93 | |||
616 | 94 | <tal:create-ppa condition="not: view/show_ppa_chooser"> | ||
617 | 95 | <input name="field.use_ppa" value="create-new" type="hidden"/> | ||
618 | 96 | <tal:widget define="widget nocall:view/widgets/ppa_name"> | ||
619 | 97 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
620 | 98 | </tal:widget> | ||
621 | 99 | </tal:create-ppa> | ||
622 | 100 | |||
623 | 101 | <tal:widget define="widget nocall:view/widgets/distros"> | ||
624 | 102 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
625 | 103 | </tal:widget> | ||
626 | 104 | <tal:widget define="widget nocall:view/widgets/recipe_text"> | ||
627 | 105 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
628 | 106 | </tal:widget> | ||
629 | 107 | |||
630 | 108 | </table> | ||
631 | 109 | </metal:formbody> | ||
632 | 110 | </div> | ||
633 | 28 | </div> | 111 | </div> |
634 | 29 | </body> | 112 | </body> |
635 | 30 | </html> | 113 | </html> |
636 | 31 | 114 | ||
637 | === modified file 'lib/lp/registry/browser/productseries.py' | |||
638 | --- lib/lp/registry/browser/productseries.py 2010-11-23 23:22:27 +0000 | |||
639 | +++ lib/lp/registry/browser/productseries.py 2010-12-14 20:58:56 +0000 | |||
640 | @@ -81,6 +81,7 @@ | |||
641 | 81 | custom_widget, | 81 | custom_widget, |
642 | 82 | LaunchpadEditFormView, | 82 | LaunchpadEditFormView, |
643 | 83 | LaunchpadFormView, | 83 | LaunchpadFormView, |
644 | 84 | render_radio_widget_part, | ||
645 | 84 | ReturnToReferrerMixin, | 85 | ReturnToReferrerMixin, |
646 | 85 | ) | 86 | ) |
647 | 86 | from lp.app.browser.tales import MenuAPI | 87 | from lp.app.browser.tales import MenuAPI |
648 | @@ -913,31 +914,19 @@ | |||
649 | 913 | def setUpWidgets(self): | 914 | def setUpWidgets(self): |
650 | 914 | """See `LaunchpadFormView`.""" | 915 | """See `LaunchpadFormView`.""" |
651 | 915 | super(ProductSeriesSetBranchView, self).setUpWidgets() | 916 | super(ProductSeriesSetBranchView, self).setUpWidgets() |
652 | 916 | |||
653 | 917 | def render(widget, term_value, current_value, label=None): | ||
654 | 918 | term = widget.vocabulary.getTerm(term_value) | ||
655 | 919 | if term.value == current_value: | ||
656 | 920 | render = widget.renderSelectedItem | ||
657 | 921 | else: | ||
658 | 922 | render = widget.renderItem | ||
659 | 923 | if label is None: | ||
660 | 924 | label = term.title | ||
661 | 925 | value = term.token | ||
662 | 926 | return render(index=term.value, | ||
663 | 927 | text=label, | ||
664 | 928 | value=value, | ||
665 | 929 | name=widget.name, | ||
666 | 930 | cssClass='') | ||
667 | 931 | |||
668 | 932 | widget = self.widgets['rcs_type'] | 917 | widget = self.widgets['rcs_type'] |
669 | 933 | vocab = widget.vocabulary | 918 | vocab = widget.vocabulary |
670 | 934 | current_value = widget._getFormValue() | 919 | current_value = widget._getFormValue() |
677 | 935 | self.rcs_type_cvs = render(widget, vocab.CVS, current_value, 'CVS') | 920 | self.rcs_type_cvs = render_radio_widget_part( |
678 | 936 | self.rcs_type_svn = render(widget, vocab.BZR_SVN, current_value, | 921 | widget, vocab.CVS, current_value, 'CVS') |
679 | 937 | 'SVN') | 922 | self.rcs_type_svn = render_radio_widget_part( |
680 | 938 | self.rcs_type_git = render(widget, vocab.GIT, current_value) | 923 | widget, vocab.BZR_SVN, current_value, 'SVN') |
681 | 939 | self.rcs_type_hg = render(widget, vocab.HG, current_value) | 924 | self.rcs_type_git = render_radio_widget_part( |
682 | 940 | self.rcs_type_bzr = render(widget, vocab.BZR, current_value) | 925 | widget, vocab.GIT, current_value) |
683 | 926 | self.rcs_type_hg = render_radio_widget_part( | ||
684 | 927 | widget, vocab.HG, current_value) | ||
685 | 928 | self.rcs_type_bzr = render_radio_widget_part( | ||
686 | 929 | widget, vocab.BZR, current_value) | ||
687 | 941 | self.rcs_type_emptymarker = widget._emptyMarker() | 930 | self.rcs_type_emptymarker = widget._emptyMarker() |
688 | 942 | 931 | ||
689 | 943 | widget = self.widgets['branch_type'] | 932 | widget = self.widgets['branch_type'] |
690 | @@ -947,7 +936,7 @@ | |||
691 | 947 | (self.branch_type_link, | 936 | (self.branch_type_link, |
692 | 948 | self.branch_type_create, | 937 | self.branch_type_create, |
693 | 949 | self.branch_type_import) = [ | 938 | self.branch_type_import) = [ |
695 | 950 | render(widget, value, current_value) | 939 | render_radio_widget_part(widget, value, current_value) |
696 | 951 | for value in (LINK_LP_BZR, CREATE_NEW, IMPORT_EXTERNAL)] | 940 | for value in (LINK_LP_BZR, CREATE_NEW, IMPORT_EXTERNAL)] |
697 | 952 | 941 | ||
698 | 953 | def _validateLinkLpBzr(self, data): | 942 | def _validateLinkLpBzr(self, data): |
699 | 954 | 943 | ||
700 | === modified file 'lib/lp/soyuz/model/archive.py' | |||
701 | --- lib/lp/soyuz/model/archive.py 2010-12-13 06:44:16 +0000 | |||
702 | +++ lib/lp/soyuz/model/archive.py 2010-12-14 20:58:56 +0000 | |||
703 | @@ -1716,7 +1716,7 @@ | |||
704 | 1716 | 1716 | ||
705 | 1717 | enabled_restricted_families = property(_getEnabledRestrictedFamilies, | 1717 | enabled_restricted_families = property(_getEnabledRestrictedFamilies, |
706 | 1718 | _setEnabledRestrictedFamilies) | 1718 | _setEnabledRestrictedFamilies) |
708 | 1719 | 1719 | ||
709 | 1720 | @classmethod | 1720 | @classmethod |
710 | 1721 | def validatePPA(self, person, proposed_name): | 1721 | def validatePPA(self, person, proposed_name): |
711 | 1722 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | 1722 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
712 | 1723 | 1723 | ||
713 | === modified file 'lib/lp/testing/__init__.py' | |||
714 | --- lib/lp/testing/__init__.py 2010-12-13 10:04:34 +0000 | |||
715 | +++ lib/lp/testing/__init__.py 2010-12-14 20:58:56 +0000 | |||
716 | @@ -715,14 +715,20 @@ | |||
717 | 715 | else: | 715 | else: |
718 | 716 | return self.getUserBrowser(url, self.user) | 716 | return self.getUserBrowser(url, self.user) |
719 | 717 | 717 | ||
725 | 718 | def getMainText( | 718 | def getMainContent(self, context, view_name=None, rootsite=None, |
726 | 719 | self, context, view_name=None, rootsite=None, no_login=False): | 719 | no_login=False): |
727 | 720 | """Return the main text of a context's page.""" | 720 | """Beautiful soup of the main content area of context's page.""" |
728 | 721 | from canonical.launchpad.testing.pages import ( | 721 | from canonical.launchpad.testing.pages import find_main_content |
724 | 722 | extract_text, find_main_content) | ||
729 | 723 | browser = self.getViewBrowser( | 722 | browser = self.getViewBrowser( |
730 | 724 | context, view_name, rootsite=rootsite, no_login=no_login) | 723 | context, view_name, rootsite=rootsite, no_login=no_login) |
732 | 725 | return extract_text(find_main_content(browser.contents)) | 724 | return find_main_content(browser.contents) |
733 | 725 | |||
734 | 726 | def getMainText(self, context, view_name=None, rootsite=None, | ||
735 | 727 | no_login=False): | ||
736 | 728 | """Return the main text of a context's page.""" | ||
737 | 729 | from canonical.launchpad.testing.pages import extract_text | ||
738 | 730 | return extract_text( | ||
739 | 731 | self.getMainContent(context, view_name, rootsite, no_login)) | ||
740 | 726 | 732 | ||
741 | 727 | 733 | ||
742 | 728 | class WindmillTestCase(TestCaseWithFactory): | 734 | class WindmillTestCase(TestCaseWithFactory): |
Hi Tim,
I'm happy with the code in this branch, with a few nitpicks. You need to
get a UI review too (and I'm not a UI reviewer). I've requested a UI review
for the MP.
82 + return render( index=term. value,
83 + text=label,
84 + value=value,
85 + name=widget.name,
86 + cssClass='')
This should be formatted as:
return render(
index= term.value, text=label, value=value, name=widget.name,
cssClass= '')
167 +USE_ARCHIVE_ VOCABULARY = SimpleVocabulary(( EXISTING_ PPA, EXISTING_PPA, CREATE_ NEW, CREATE_NEW,
168 + SimpleTerm(
169 + _("Use an existing PPA")),
170 + SimpleTerm(
171 + _("Create a new PPA for this recipe")),
172 + ))
This should be formatted:
USE_ARCHIVE_ VOCABULARY = SimpleVocabulary(( EXISTING_ PPA, EXISTING_PPA, _("Use an existing PPA")),
SimpleTerm(
SimpleTerm(
CREATE_NEW, CREATE_NEW, _("Create a new PPA for this recipe")),
))
183 + ppa_name = TextLine( name_validator,
184 + title=_("New PPA name"), required=False,
185 + constraint=
186 + description=_("A new PPA with this name will be created for "
187 + "the owner of the recipe ."))
The arguments here should be indented by one level from the declaration,
not two.
A couple of JS points that I'm not too worried about (but decided to
point out all the same):
444 + function set_enabled( field_id, is_enabled) { field_id) ;
445 + var field = Y.DOM.byId(
446 + field.disabled = !is_enabled;
447 + if (is_enabled && set_field_focus) field.focus();
448 + };
You don't need a semicolon here.
450 + module. onclick_ use_ppa = function(e) { dValue( 'input[ name=field. use_ppa] '); PPA_NAME_ ID, true); PPA_SELECTOR_ ID, false); PPA_NAME_ ID, false); PPA_SELECTOR_ ID, true);
451 + var value = getRadioSelecte
452 + if (value == 'create-new') {
453 + set_enabled(
454 + set_enabled(
455 + }
456 + else {
457 + set_enabled(
458 + set_enabled(
459 + }
460 + }
But technically you *do* need one here, since it's part of an
assignment. Ain't JS fun?