Merge lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Leonard Richardson
Approved revision: no longer in the source branch.
Merged at revision: 12251
Proposed branch: lp:~thumper/launchpad/inline-ppa-chooser
Merge into: lp:launchpad
Prerequisite: lp:~thumper/launchpad/recipe-inline-edit-owner
Diff against target: 683 lines (+193/-116)
16 files modified
lib/canonical/launchpad/doc/lazr-js-widgets.txt (+2/-2)
lib/canonical/widgets/lazrjs.py (+35/-21)
lib/canonical/widgets/templates/inline-picker.pt (+7/-5)
lib/canonical/widgets/tests/test_inlineeditpickerwidget.py (+57/-0)
lib/lp/app/browser/configure.zcml (+6/-0)
lib/lp/app/browser/tales.py (+4/-1)
lib/lp/app/browser/webservice.py (+14/-11)
lib/lp/app/javascript/picker.js (+12/-14)
lib/lp/bugs/javascript/bugtask_index.js (+1/-9)
lib/lp/code/browser/sourcepackagerecipe.py (+32/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+5/-5)
lib/lp/code/model/sourcepackagerecipebuild.py (+4/-1)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+11/-3)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+2/-36)
lib/lp/registry/browser/configure.zcml (+0/-5)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~thumper/launchpad/inline-ppa-chooser
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+46983@code.launchpad.net

Commit message

[r=leonardr][ui=none][bug=423149] Allow editing of the recipe's daily build PPA

Description of the change

The primary purpose of this branch is to add an inline picker
for the daily build PPA. This brought around much refactoring
and simplification.

lib/canonical/widgets/lazrjs.py
 - cleaned up the docstring for the InlineEditPickerWidget
 - renamed some parameters and removed the jsonification of
   some where it was wrong
lib/canonical/widgets/templates/inline-picker.pt
 - changed the registration code to run on load
lib/canonical/widgets/tests/test_inlineeditpickerwidget.py
 - added tests for the InlineEditPickerWidget

lib/lp/registry/browser/webservice.py => lib/lp/app/browser/webservice.py
lib/lp/registry/browser/webservice.py
lib/lp/app/browser/webservice.py
 - moved the person_xhtml_representation to from lp.registry
   to lp.app and made more generic, and renamed to
   reference_xhtml_representation
 - moved the text_xhtml_representation to lp.app

lib/lp/app/browser/tales.py
 - tweaked format_link to raise if link isn't supported

lib/lp/app/javascript/picker.js
 - renamed non_searchable_vocabulary to show_search_box
 - fixed up the config value existance checks
 - fixed the code where it was checking to see of the content
   uri had changed to use the full_resource_uri

lib/lp/bugs/javascript/bugtask_index.js
 - Remove the webkit special casing for product editing on
   bugtask rows

lib/lp/code/browser/sourcepackagerecipe.py
 - show that daily builds that haven't specified a PPA will
   not get run.
 - add InlineEditPickerWidgets for person and archive

lib/lp/code/model/sourcepackagerecipebuild.py
 - have the daily build code handle the case where the recipe
   doesn't have an archive specified

lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
 - and test it here

lib/lp/code/templates/sourcepackagerecipe-index.pt
 - clean up the page template now that we are using the widget

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/lazr-js-widgets.txt'
2--- lib/canonical/launchpad/doc/lazr-js-widgets.txt 2010-11-08 14:15:01 +0000
3+++ lib/canonical/launchpad/doc/lazr-js-widgets.txt 2011-01-21 04:44:21 +0000
4@@ -122,8 +122,8 @@
5 </span>
6 ...Y.lp.app.picker.addPickerPatcher(...
7
8-The resource_uri is the canonical_url for the object in
9+The json_resource_uri is the canonical_url for the object in
10 WebServiceClientRequests.
11
12- >>> print widget.resource_uri
13+ >>> print widget.json_resource_uri
14 "/firefox/+bug/1"
15
16=== modified file 'lib/canonical/widgets/lazrjs.py'
17--- lib/canonical/widgets/lazrjs.py 2011-01-21 04:43:16 +0000
18+++ lib/canonical/widgets/lazrjs.py 2011-01-21 04:44:21 +0000
19@@ -18,6 +18,7 @@
20 from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
21 from zope.component import getUtility
22 from zope.security.checker import canAccess, canWrite
23+from zope.schema.interfaces import IVocabulary
24 from zope.schema.vocabulary import getVocabularyRegistry
25
26 from lazr.restful.interfaces import IWebServiceClientRequest
27@@ -26,6 +27,7 @@
28 from canonical.launchpad.webapp.interfaces import ILaunchBag
29 from canonical.launchpad.webapp.publisher import canonical_url
30 from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
31+from lp.services.propertycache import cachedproperty
32
33
34 class TextLineEditorWidget:
35@@ -260,21 +262,22 @@
36 __call__ = ViewPageTemplateFile('templates/inline-picker.pt')
37
38 def __init__(self, context, request, interface_attribute, default_html,
39- id=None, header='Select an item', step_title='Search',
40- remove_button_text='Remove', null_display_value='None'):
41+ content_box_id=None, header='Select an item',
42+ step_title='Search', remove_button_text='Remove',
43+ null_display_value='None'):
44 """Create a widget wrapper.
45
46 :param context: The object that is being edited.
47 :param request: The request object.
48- :param interface_attribute: The attribute being edited.
49+ :param interface_attribute: The attribute being edited. This should be
50+ a field from an interface of the form ISomeInterface['fieldname']
51 :param default_html: Default display of attribute.
52- :param id: The HTML id to use for this widget. Automatically
53+ :param content_box_id: The HTML id to use for this widget. Automatically
54 generated if this is not provided.
55 :param header: The large text at the top of the picker.
56 :param step_title: Smaller line of text below the header.
57- :param show_remove_button: Show remove button below search box.
58- :param show_assign_me_button: Show assign-me button below search box.
59 :param remove_button_text: Override default button text: "Remove"
60+ :param null_display_value: This will be shown for a missing value
61 """
62 self.context = context
63 self.request = request
64@@ -282,10 +285,10 @@
65 self.interface_attribute = interface_attribute
66 self.attribute_name = interface_attribute.__name__
67
68- if id is None:
69- self.id = self._generate_id()
70+ if content_box_id is None:
71+ self.content_box_id = self._generate_id()
72 else:
73- self.id = id
74+ self.content_box_id = content_box_id
75
76 self.header = header
77 self.step_title = step_title
78@@ -293,12 +296,11 @@
79 self.null_display_value = null_display_value
80
81 # JSON encoded attributes.
82- self.json_id = simplejson.dumps(self.id)
83+ self.json_content_box_id = simplejson.dumps(self.content_box_id)
84 self.json_attribute = simplejson.dumps(self.attribute_name + '_link')
85- self.vocabulary_name = simplejson.dumps(
86+ self.json_vocabulary_name = simplejson.dumps(
87 self.interface_attribute.vocabularyName)
88- self.show_remove_button = simplejson.dumps(
89- not self.interface_attribute.required)
90+ self.show_remove_button = not self.interface_attribute.required
91
92 @property
93 def config(self):
94@@ -307,16 +309,25 @@
95 remove_button_text=self.remove_button_text,
96 null_display_value=self.null_display_value,
97 show_remove_button=self.show_remove_button,
98- show_assign_me_button=self.show_assign_me_button))
99+ show_assign_me_button=self.show_assign_me_button,
100+ show_search_box=self.show_search_box))
101+
102+ @cachedproperty
103+ def vocabulary(self):
104+ registry = getVocabularyRegistry()
105+ return registry.get(
106+ IVocabulary, self.interface_attribute.vocabularyName)
107+
108+ @property
109+ def show_search_box(self):
110+ return IHugeVocabulary.providedBy(self.vocabulary)
111
112 @property
113 def show_assign_me_button(self):
114 # show_assign_me_button is true if user is in the vocabulary.
115- registry = getVocabularyRegistry()
116- vocabulary = registry.get(
117- IHugeVocabulary, self.interface_attribute.vocabularyName)
118+ vocabulary = self.vocabulary
119 user = getUtility(ILaunchBag).user
120- return simplejson.dumps(user and user in vocabulary)
121+ return user and user in vocabulary
122
123 @classmethod
124 def _generate_id(cls):
125@@ -325,7 +336,7 @@
126 return 'inline-picker-activator-id-%d' % cls.last_id
127
128 @property
129- def resource_uri(self):
130+ def json_resource_uri(self):
131 return simplejson.dumps(
132 canonical_url(
133 self.context, request=IWebServiceClientRequest(self.request),
134@@ -343,8 +354,11 @@
135 # We look at the top of the annotation stack, since Ajax
136 # requests always go to the most recent version of the web
137 # service.
138- exported_tag_stack = self.interface_attribute.getTaggedValue(
139- 'lazr.restful.exported')
140+ try:
141+ exported_tag_stack = self.interface_attribute.getTaggedValue(
142+ 'lazr.restful.exported')
143+ except KeyError:
144+ return False
145 mutator_info = exported_tag_stack.get('mutator_annotations')
146 if mutator_info is not None:
147 mutator_method, mutator_extra = mutator_info
148
149=== modified file 'lib/canonical/widgets/templates/inline-picker.pt'
150--- lib/canonical/widgets/templates/inline-picker.pt 2011-01-21 04:43:16 +0000
151+++ lib/canonical/widgets/templates/inline-picker.pt 2011-01-21 04:44:21 +0000
152@@ -1,4 +1,4 @@
153-<span tal:attributes="id view/id">
154+<span tal:attributes="id view/content_box_id">
155 <span class="yui3-activator-data-box">
156 <tal:attribute replace="structure view/default_html"/>
157 </span>
158@@ -14,11 +14,13 @@
159 return;
160 }
161
162- Y.lp.app.picker.addPickerPatcher(
163- ${view/vocabulary_name},
164- ${view/resource_uri},
165+ Y.on('load', function(e) {
166+ Y.lp.app.picker.addPickerPatcher(
167+ ${view/json_vocabulary_name},
168+ ${view/json_resource_uri},
169 ${view/json_attribute},
170- ${view/json_id},
171+ ${view/json_content_box_id},
172 ${view/config});
173+ }, window);
174 });
175 "/>
176
177=== renamed directory 'lib/canonical/widgets/ftests' => 'lib/canonical/widgets/tests'
178=== added file 'lib/canonical/widgets/tests/test_inlineeditpickerwidget.py'
179--- lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 1970-01-01 00:00:00 +0000
180+++ lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 2011-01-21 04:44:21 +0000
181@@ -0,0 +1,57 @@
182+# Copyright 2010 Canonical Ltd. This software is licensed under the
183+# GNU Affero General Public License version 3 (see the file LICENSE).
184+
185+"""Tests for the InlineEditPickerWidget."""
186+
187+__metaclass__ = type
188+
189+from zope.interface import Interface
190+from zope.schema import Choice
191+
192+from canonical.testing.layers import DatabaseFunctionalLayer
193+from canonical.widgets.lazrjs import InlineEditPickerWidget
194+from lp.testing import (
195+ login_person,
196+ TestCaseWithFactory,
197+ )
198+
199+
200+class TestInlineEditPickerWidget(TestCaseWithFactory):
201+
202+ layer = DatabaseFunctionalLayer
203+
204+ def getWidget(self, **kwargs):
205+ class ITest(Interface):
206+ test_field = Choice(**kwargs)
207+ return InlineEditPickerWidget(None, None, ITest['test_field'], None)
208+
209+ def test_huge_vocabulary_is_searchable(self):
210+ # Make sure that when given a field for a huge vocabulary, the picker
211+ # is set to show the search box.
212+ widget = self.getWidget(vocabulary='ValidPersonOrTeam')
213+ self.assertTrue(widget.show_search_box)
214+
215+ def test_normal_vocabulary_is_not_searchable(self):
216+ # Make sure that when given a field for a normal vocabulary, the picker
217+ # is set to show the search box.
218+ widget = self.getWidget(vocabulary='UserTeamsParticipation')
219+ self.assertFalse(widget.show_search_box)
220+
221+ def test_required_fields_dont_have_a_remove_link(self):
222+ widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
223+ self.assertFalse(widget.show_remove_button)
224+
225+ def test_optional_fields_do_have_a_remove_link(self):
226+ widget = self.getWidget(
227+ vocabulary='ValidPersonOrTeam', required=False)
228+ self.assertTrue(widget.show_remove_button)
229+
230+ def test_assign_me_exists_if_user_in_vocabulary(self):
231+ widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
232+ login_person(self.factory.makePerson())
233+ self.assertTrue(widget.show_assign_me_button)
234+
235+ def test_assign_me_not_shown_if_user_not_in_vocabulary(self):
236+ widget = self.getWidget(vocabulary='TargetPPAs', required=True)
237+ login_person(self.factory.makePerson())
238+ self.assertFalse(widget.show_assign_me_button)
239
240=== modified file 'lib/lp/app/browser/configure.zcml'
241--- lib/lp/app/browser/configure.zcml 2011-01-06 01:36:25 +0000
242+++ lib/lp/app/browser/configure.zcml 2011-01-21 04:44:21 +0000
243@@ -556,4 +556,10 @@
244 <adapter
245 factory="lp.app.browser.tales.LaunchpadLayerToMainTemplateAdapter"
246 />
247+
248+ <adapter
249+ factory="lp.app.browser.webservice.reference_xhtml_representation"/>
250+ <adapter
251+ factory="lp.app.browser.webservice.text_xhtml_representation"/>
252+
253 </configure>
254
255=== modified file 'lib/lp/app/browser/tales.py'
256--- lib/lp/app/browser/tales.py 2011-01-14 10:06:25 +0000
257+++ lib/lp/app/browser/tales.py 2011-01-21 04:44:21 +0000
258@@ -78,7 +78,10 @@
259 def format_link(obj, view_name=None):
260 """Return the equivalent of obj/fmt:link as a string."""
261 adapter = queryAdapter(obj, IPathAdapter, 'fmt')
262- return adapter.link(view_name)
263+ link = getattr(adapter, 'link', None)
264+ if link is None:
265+ raise NotImplementedError("Missing link function on adapter.")
266+ return link(view_name)
267
268
269 class MenuLinksDict(dict):
270
271=== renamed file 'lib/lp/registry/browser/tests/test_webservice.py' => 'lib/lp/app/browser/tests/test_webservice.py'
272=== renamed file 'lib/lp/registry/browser/webservice.py' => 'lib/lp/app/browser/webservice.py'
273--- lib/lp/registry/browser/webservice.py 2011-01-21 04:43:16 +0000
274+++ lib/lp/app/browser/webservice.py 2011-01-21 04:44:21 +0000
275@@ -8,6 +8,7 @@
276
277 from lazr.restful.interfaces import (
278 IFieldHTMLRenderer,
279+ IReference,
280 IWebServiceClientRequest,
281 )
282 from zope import component
283@@ -18,22 +19,24 @@
284 from zope.schema.interfaces import IText
285
286 from lp.app.browser.stringformatter import FormattersAPI
287-from lp.app.browser.tales import PersonFormatterAPI
288-from lp.services.fields import IPersonChoice
289-
290-
291-@component.adapter(Interface, IPersonChoice, IWebServiceClientRequest)
292+from lp.app.browser.tales import format_link
293+
294+
295+@component.adapter(Interface, IReference, IWebServiceClientRequest)
296 @implementer(IFieldHTMLRenderer)
297-def person_xhtml_representation(context, field, request):
298- """Render a person as a link to the person."""
299+def reference_xhtml_representation(context, field, request):
300+ """Render an object as a link to the object."""
301
302 def render(value):
303- # The value is a webservice link to a person.
304- person = getattr(context, field.__name__, None)
305- if person is None:
306+ # The value is a webservice link to the object, we want field value.
307+ obj = getattr(context, field.__name__, None)
308+ if obj is None:
309 return ''
310 else:
311- return PersonFormatterAPI(person).link(None)
312+ try:
313+ return format_link(obj)
314+ except NotImplementedError:
315+ return value
316 return render
317
318
319
320=== modified file 'lib/lp/app/javascript/picker.js'
321--- lib/lp/app/javascript/picker.js 2011-01-21 04:43:16 +0000
322+++ lib/lp/app/javascript/picker.js 2011-01-21 04:44:21 +0000
323@@ -23,10 +23,8 @@
324 * Defaults to false, should be a boolean.
325 * config.show_assign_me_botton: Should the 'assign me' button be shown?
326 * Defaults to false, should be a boolean.
327- * config.non_searchable_vocabulary: No search bar is shown, and the
328- * vocabulary is required to not implement IHugeVocabulary. The
329- * vocabularies values are then offered in a batched way for
330- * selection. Defaults to false.
331+ * config.show_search_box: Should the search box be shown.
332+ * Vocabularies that are not huge should not have a search box.
333 */
334 namespace.addPickerPatcher = function (
335 vocabulary, resource_uri, attribute_name,
336@@ -40,30 +38,30 @@
337 var show_assign_me_button = false;
338 var remove_button_text = 'Remove';
339 var null_display_value = 'None';
340- var non_searchable_vocabulary = false;
341+ var show_search_box = true;
342 var full_resource_uri = LP.client.get_absolute_uri(resource_uri);
343 var current_context_uri = LP.client.cache['context']['self_link'];
344 var editing_main_context = (full_resource_uri == current_context_uri);
345
346 if (config !== undefined) {
347- if (config.remove_button_text) {
348+ if (config.remove_button_text !== undefined) {
349 remove_button_text = config.remove_button_text;
350 }
351
352- if (config.null_display_value) {
353+ if (config.null_display_value !== undefined) {
354 null_display_value = config.null_display_value;
355 }
356
357- if (config.show_remove_button) {
358+ if (config.show_remove_button !== undefined) {
359 show_remove_button = config.show_remove_button;
360 }
361
362- if (config.show_assign_me_button) {
363+ if (config.show_assign_me_button !== undefined) {
364 show_assign_me_button = config.show_assign_me_button;
365 }
366
367- if (config.non_searchable_vocabulary) {
368- non_searchable_vocabulary = config.non_searchable_vocabulary;
369+ if (config.show_search_box !== undefined) {
370+ show_search_box = config.show_search_box;
371 }
372 }
373
374@@ -125,7 +123,7 @@
375 } else if (current_field == 'self_link') {
376 picker._resource_uri = element.get('innerHTML');
377 content_uri_has_changed = (
378- resource_uri != picker._resource_uri);
379+ full_resource_uri != picker._resource_uri);
380 }
381 }
382 });
383@@ -139,7 +137,7 @@
384 // fix this.
385 var new_url = picker._resource_uri.replace('/api/devel', '');
386 window.location = new_url;
387- }
388+ }
389 };
390
391 var patch_payload = {};
392@@ -215,7 +213,7 @@
393 picker.set('footer_slot', extra_buttons);
394
395 activator.subscribe('act', function (e) {
396- if (non_searchable_vocabulary) {
397+ if (!show_search_box) {
398 picker.set('min_search_chars', 0);
399 picker.fire('search', '');
400 picker.get('contentBox').one('.yui3-picker-search-box').addClass('unseen');
401
402=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
403--- lib/lp/bugs/javascript/bugtask_index.js 2011-01-21 04:43:16 +0000
404+++ lib/lp/bugs/javascript/bugtask_index.js 2011-01-21 04:44:21 +0000
405@@ -1362,21 +1362,13 @@
406 (LP.client.links.me !== null)) {
407 if (Y.Lang.isValue(bugtarget_content)) {
408 if (conf.target_is_product) {
409- if (Y.UA.webkit) {
410- bugtarget_content.replaceChild(
411- Y.DOM.create(
412- '<a href="+editstatus" ' +
413- ' class="sprite edit yui3-activator-act" />'),
414- bugtarget_content.one('.yui3-activator-act'));
415- } else {
416- var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
417+ var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
418 'Product',
419 conf.bugtask_path,
420 "target_link",
421 bugtarget_content.get('id'),
422 {"step_title": "Search products",
423 "header": "Change product"});
424- }
425 }
426 }
427
428
429=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
430--- lib/lp/code/browser/sourcepackagerecipe.py 2011-01-18 18:44:11 +0000
431+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-01-21 04:44:21 +0000
432@@ -58,11 +58,12 @@
433 )
434 from canonical.launchpad.webapp.authorization import check_permission
435 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
436-from canonical.widgets.suggestion import RecipeOwnerWidget
437 from canonical.widgets.itemswidgets import (
438 LabeledMultiCheckBoxWidget,
439 LaunchpadRadioWidget,
440 )
441+from canonical.widgets.lazrjs import InlineEditPickerWidget
442+from canonical.widgets.suggestion import RecipeOwnerWidget
443 from lp.app.browser.launchpadform import (
444 action,
445 custom_widget,
446@@ -182,7 +183,12 @@
447 super(SourcePackageRecipeView, self).initialize()
448 self.request.response.addWarningNotification(RECIPE_BETA_MESSAGE)
449 recipe = self.context
450- if self.dailyBuildWithoutUploadPermission():
451+ if recipe.build_daily and recipe.daily_build_archive is None:
452+ self.request.response.addWarningNotification(
453+ structured(
454+ "Daily builds for this recipe will <strong>not</strong> "
455+ "occur.<br/><br/>There is no PPA."))
456+ elif self.dailyBuildWithoutUploadPermission():
457 self.request.response.addWarningNotification(
458 structured(
459 "Daily builds for this recipe will <strong>not</strong> "
460@@ -226,6 +232,30 @@
461 return not has_upload
462 return False
463
464+ @property
465+ def person_picker(self):
466+ return InlineEditPickerWidget(
467+ self.context, self.request, ISourcePackageRecipe['owner'],
468+ format_link(self.context.owner),
469+ content_box_id='recipe-owner',
470+ header='Change owner',
471+ step_title='Select a new owner')
472+
473+ @property
474+ def archive_picker(self):
475+ ppa = self.context.daily_build_archive
476+ if ppa is None:
477+ initial_html = 'None'
478+ else:
479+ initial_html = format_link(ppa)
480+ return InlineEditPickerWidget(
481+ self.context, self.request,
482+ ISourcePackageAddSchema['daily_build_archive'],
483+ initial_html,
484+ content_box_id='recipe-ppa',
485+ header='Change daily build archive',
486+ step_title='Select a PPA')
487+
488
489 class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
490 """A view for requesting builds of a SourcePackageRecipe."""
491
492=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
493--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-01-21 04:43:16 +0000
494+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-01-21 04:44:21 +0000
495@@ -156,7 +156,7 @@
496 Owner: Master Chef Edit
497 Base branch: lp://dev/~chef/ratatouille/veggies
498 Debian version: {debupstream}-0~{revno}
499- Daily build archive: Secret PPA
500+ Daily build archive: Secret PPA Edit
501 Distribution series: Secret Squirrel
502 .*
503
504@@ -608,7 +608,7 @@
505 Base branch: lp://dev/~chef/ratatouille/meat
506 Debian version: {debupstream}-0~{revno}
507 Daily build archive:
508- PPA 2
509+ PPA 2 Edit
510 Distribution series: Mumbly Midget
511 .*
512
513@@ -668,7 +668,7 @@
514 Base branch: lp://dev/~chef/ratatouille/meat
515 Debian version: {debupstream}-0~{revno}
516 Daily build archive:
517- Secret PPA
518+ Secret PPA Edit
519 Distribution series: Mumbly Midget
520 .*
521
522@@ -808,7 +808,7 @@
523 Base branch: lp://dev/~chef/ratatouille/meat
524 Debian version: {debupstream}-0~{revno}
525 Daily build archive:
526- Secret PPA
527+ Secret PPA Edit
528 Distribution series: Mumbly Midget
529 .*
530
531@@ -858,7 +858,7 @@
532 Owner: Master Chef Edit
533 Base branch: lp://dev/~chef/chocolate/cake
534 Debian version: {debupstream}-0~{revno}
535- Daily build archive: Secret PPA
536+ Daily build archive: Secret PPA Edit
537 Distribution series: Secret Squirrel
538
539 Latest builds
540
541=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
542--- lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-15 06:32:40 +0000
543+++ lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-21 04:44:21 +0000
544@@ -194,8 +194,12 @@
545 logger = logging.getLogger()
546 builds = []
547 for recipe in recipes:
548+ recipe.is_stale = False
549 logger.debug(
550 'Recipe %s/%s is stale', recipe.owner.name, recipe.name)
551+ if recipe.daily_build_archive is None:
552+ logger.debug(' - No daily build archive specified.')
553+ continue
554 for distroseries in recipe.distroseries:
555 series_name = distroseries.named_version
556 try:
557@@ -215,7 +219,6 @@
558 else:
559 logger.debug(' - build requested for %s', series_name)
560 builds.append(build)
561- recipe.is_stale = False
562 return builds
563
564 def _unqueueBuild(self):
565
566=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
567--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-10 03:22:42 +0000
568+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-21 04:44:21 +0000
569@@ -235,13 +235,21 @@
570 self.assertIs(None, build.manifest)
571
572 def test_makeDailyBuilds(self):
573- self.assertEqual([],
574- SourcePackageRecipeBuild.makeDailyBuilds())
575+ self.assertEqual([], SourcePackageRecipeBuild.makeDailyBuilds())
576 recipe = self.factory.makeSourcePackageRecipe(build_daily=True)
577- build = SourcePackageRecipeBuild.makeDailyBuilds()[0]
578+ [build] = SourcePackageRecipeBuild.makeDailyBuilds()
579 self.assertEqual(recipe, build.recipe)
580 self.assertEqual(list(recipe.distroseries), [build.distroseries])
581
582+ def test_makeDailyBuilds_skips_missing_archive(self):
583+ """When creating daily builds, skip ones that are already pending."""
584+ recipe = self.factory.makeSourcePackageRecipe(
585+ build_daily=True, is_stale=True)
586+ with person_logged_in(recipe.owner):
587+ recipe.daily_build_archive = None
588+ builds = SourcePackageRecipeBuild.makeDailyBuilds()
589+ self.assertEqual([], builds)
590+
591 def test_makeDailyBuilds_logs_builds(self):
592 # If a logger is passed into the makeDailyBuilds method, each recipe
593 # that a build is requested for gets logged.
594
595=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
596--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-01-21 04:43:16 +0000
597+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2011-01-21 04:44:21 +0000
598@@ -13,27 +13,6 @@
599 padding-left: 2em;
600 }
601 </style>
602- <script type="text/javascript"
603- tal:content="string:
604- LPS.use('lp.app.picker', function(Y) {
605- Y.on('load', function(e) {
606-
607- var config = {
608- header: 'Change owner',
609- step_title: 'Select a new owner',
610- non_searchable_vocabulary: true
611- };
612-
613- Y.lp.app.picker.addPickerPatcher(
614- 'UserTeamsParticipationPlusSelf',
615- LP.client.cache['context']['self_link'],
616- 'owner_link',
617- 'recipe-owner',
618- config);
619-
620- }, window);
621- });
622- "/>
623 </metal:block>
624
625 <body>
626@@ -76,17 +55,7 @@
627
628 <dl id="owner">
629 <dt>Owner:</dt>
630- <dd>
631- <span id="recipe-owner">
632- <span class="yui3-activator-data-box">
633- <tal:owner replace="structure context/owner/fmt:link" />
634- </span>
635- <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
636- tal:condition="context/required:launchpad.Edit">
637- Edit
638- </button>
639- <div class="yui3-activator-message-box yui3-activator-hidden" />
640- </span>
641+ <dd tal:content="structure view/person_picker"/>
642 </dl>
643 <dl id="base-branch">
644 <dt>Base branch:</dt>
645@@ -98,10 +67,7 @@
646 </dl>
647 <dl id="daily_build_archive">
648 <dt>Daily build archive:</dt>
649- <dd tal:content="structure context/daily_build_archive/fmt:link"
650- tal:condition="context/daily_build_archive">
651- </dd>
652- <dd tal:condition="not: context/daily_build_archive">None</dd>
653+ <dd tal:content="structure view/archive_picker"/>
654 </dl>
655
656 <dl id="distros">
657
658=== modified file 'lib/lp/registry/browser/configure.zcml'
659--- lib/lp/registry/browser/configure.zcml 2011-01-19 22:53:32 +0000
660+++ lib/lp/registry/browser/configure.zcml 2011-01-21 04:44:21 +0000
661@@ -2328,9 +2328,4 @@
662 rootsite="api"
663 attribute_to_parent="owner" />
664
665- <adapter
666- factory="lp.registry.browser.webservice.person_xhtml_representation"/>
667- <adapter
668- factory="lp.registry.browser.webservice.text_xhtml_representation"/>
669-
670 </configure>
671
672=== modified file 'versions.cfg'
673--- versions.cfg 2011-01-21 04:43:16 +0000
674+++ versions.cfg 2011-01-21 04:44:21 +0000
675@@ -33,7 +33,7 @@
676 lazr.delegates = 1.2.0
677 lazr.enum = 1.1.2
678 lazr.lifecycle = 1.1
679-lazr.restful = 0.15.1
680+lazr.restful = 0.15.2
681 lazr.restfulclient = 0.11.1
682 lazr.smtptest = 1.1
683 lazr.testing = 0.1.1