Merge lp:~cjwatson/launchpad/custom-widget-no-class-advice-1 into lp:launchpad

Proposed by Colin Watson on 2018-07-15
Status: Merged
Merged at revision: 18747
Proposed branch: lp:~cjwatson/launchpad/custom-widget-no-class-advice-1
Merge into: lp:launchpad
Diff against target: 645 lines (+116/-94)
13 files modified
lib/lp/answers/browser/faqtarget.py (+2/-3)
lib/lp/answers/browser/question.py (+24/-16)
lib/lp/answers/browser/questiontarget.py (+11/-9)
lib/lp/app/browser/doc/launchpadform-view.txt (+6/-6)
lib/lp/app/browser/launchpad.py (+3/-6)
lib/lp/app/browser/launchpadform.py (+19/-9)
lib/lp/app/browser/multistep.py (+4/-3)
lib/lp/app/browser/tests/test_launchpadform_doc.py (+6/-7)
lib/lp/app/doc/launchpadform.txt (+9/-8)
lib/lp/app/widgets/doc/image-widget.txt (+1/-1)
lib/lp/blueprints/browser/specification.py (+11/-10)
lib/lp/blueprints/browser/sprint.py (+14/-10)
lib/lp/blueprints/browser/sprintattendance.py (+6/-6)
To merge this branch: bzr merge lp:~cjwatson/launchpad/custom-widget-no-class-advice-1
Reviewer Review Type Date Requested Status
William Grant code 2018-07-15 Approve on 2018-07-30
Review via email: mp+349631@code.launchpad.net

Commit message

Start removing Zope class advice from custom widget registration.

Description of the change

As in https://code.launchpad.net/~cjwatson/launchpad/traversal-no-class-advice/+merge/349625, Zope class advice doesn't work in Python 3. The replacement is less obvious here. I considered and rejected some alternatives:

 * Adding methods just in order to be able to decorate them was far too verbose.
 * Writing out custom_widgets dictionaries in each view almost worked, but it got cumbersome in the cases where one view inherits from another that also has custom widgets.

In the end I decided that the most concise and readable option was to use separate attributes for each custom widget with formulaic names. With this, just using CustomWidgetFactory directly isn't too bad, although I added a bit of sugar to avoid needing to write that out in views in cases where no arguments need to be passed.

After this, I'll have a few more branches to convert batches of views, and then we can remove the class advisor.

To post a comment you must log in.
William Grant (wgrant) wrote :

I really don't like this, but I can't see a better option.

review: Approve (code)
18733. By Colin Watson on 2018-08-09

Remove redundant assignment.

18734. By Colin Watson on 2018-08-09

Add IWidgetFactory check to QuestionAddView._getFieldsForWidgets.

This isn't strictly needed as all this view's custom widgets already provide
IWidgetFactory, but it removes a possible footgun for future developers.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/browser/faqtarget.py'
2--- lib/lp/answers/browser/faqtarget.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/answers/browser/faqtarget.py 2018-08-09 15:10:30 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """`IFAQTarget` browser views."""
10@@ -14,7 +14,6 @@
11 from lp.answers.interfaces.faq import IFAQ
12 from lp.app.browser.launchpadform import (
13 action,
14- custom_widget,
15 LaunchpadFormView,
16 )
17 from lp.app.errors import NotFoundError
18@@ -45,7 +44,7 @@
19 label = _('Create a new FAQ')
20 field_names = ['title', 'keywords', 'content']
21
22- custom_widget('keywords', TokensTextWidget)
23+ custom_widget_keywords = TokensTextWidget
24
25 @property
26 def page_title(self):
27
28=== modified file 'lib/lp/answers/browser/question.py'
29--- lib/lp/answers/browser/question.py 2016-01-26 15:47:37 +0000
30+++ lib/lp/answers/browser/question.py 2018-08-09 15:10:30 +0000
31@@ -1,4 +1,4 @@
32-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
33+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
34 # GNU Affero General Public License version 3 (see the file LICENSE).
35
36 """Question views."""
37@@ -37,7 +37,11 @@
38 from zope.component import getUtility
39 from zope.event import notify
40 from zope.formlib import form
41-from zope.formlib.widget import renderElement
42+from zope.formlib.interfaces import IWidgetFactory
43+from zope.formlib.widget import (
44+ CustomWidgetFactory,
45+ renderElement,
46+ )
47 from zope.formlib.widgets import (
48 TextAreaWidget,
49 TextWidget,
50@@ -78,7 +82,6 @@
51 from lp.answers.vocabulary import UsesAnswersDistributionVocabulary
52 from lp.app.browser.launchpadform import (
53 action,
54- custom_widget,
55 LaunchpadEditFormView,
56 LaunchpadFormView,
57 safe_action,
58@@ -274,7 +277,7 @@
59 """View for the Answer Tracker index page."""
60
61 schema = IAnswersFrontPageSearchForm
62- custom_widget('scope', ProjectScopeWidget)
63+ custom_widget_scope = ProjectScopeWidget
64
65 page_title = 'Launchpad Answers'
66 label = 'Questions and Answers'
67@@ -560,7 +563,8 @@
68 # The fields displayed on the search page.
69 search_field_names = ['language', 'title']
70
71- custom_widget('title', TextWidget, displayWidth=40, displayMaxWidth=250)
72+ custom_widget_title = CustomWidgetFactory(
73+ TextWidget, displayWidth=40, displayMaxWidth=250)
74
75 search_template = ViewPageTemplateFile(
76 '../templates/question-add-search.pt')
77@@ -603,8 +607,13 @@
78 else:
79 fields = self.form_fields
80 for field in fields:
81- if field.__name__ in self.custom_widgets:
82- field.custom_widget = self.custom_widgets[field.__name__]
83+ widget = getattr(self, 'custom_widget_%s' % field.__name__, None)
84+ if widget is not None:
85+ if IWidgetFactory.providedBy(widget):
86+ field.custom_widget = widget
87+ else:
88+ # Allow views to save some typing in common cases.
89+ field.custom_widget = CustomWidgetFactory(widget)
90 return fields
91
92 def setUpWidgets(self):
93@@ -755,9 +764,9 @@
94 "language", "title", "description", "target", "assignee",
95 "whiteboard"]
96
97- custom_widget('title', TextWidget, displayWidth=40)
98- custom_widget('whiteboard', TextAreaWidget, height=5)
99- custom_widget('target', QuestionTargetWidget)
100+ custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=40)
101+ custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
102+ custom_widget_target = QuestionTargetWidget
103
104 @property
105 def page_title(self):
106@@ -1239,8 +1248,8 @@
107
108 field_names = ['title', 'keywords', 'content']
109
110- custom_widget('keywords', TokensTextWidget)
111- custom_widget("message", TextAreaWidget, height=5)
112+ custom_widget_keywords = TokensTextWidget
113+ custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
114
115 @property
116 def initial_values(self):
117@@ -1262,8 +1271,7 @@
118 copy_field(IQuestionLinkFAQForm['message']))
119 self.form_fields['message'].field.title = _(
120 'Additional comment for question #%s' % self.context.id)
121- self.form_fields['message'].custom_widget = (
122- self.custom_widgets['message'])
123+ self.form_fields['message'].custom_widget = self.custom_widget_message
124
125 @action(_('Create and Link'), name='create_and_link')
126 def create_and_link_action(self, action, data):
127@@ -1418,9 +1426,9 @@
128
129 schema = IQuestionLinkFAQForm
130
131- custom_widget('faq', SearchableFAQRadioWidget)
132+ custom_widget_faq = SearchableFAQRadioWidget
133
134- custom_widget("message", TextAreaWidget, height=5)
135+ custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
136
137 label = _('Is this a FAQ?')
138
139
140=== modified file 'lib/lp/answers/browser/questiontarget.py'
141--- lib/lp/answers/browser/questiontarget.py 2016-01-26 15:47:37 +0000
142+++ lib/lp/answers/browser/questiontarget.py 2018-08-09 15:10:30 +0000
143@@ -1,4 +1,4 @@
144-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
145+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
146 # GNU Affero General Public License version 3 (see the file LICENSE).
147
148 """IQuestionTarget browser views."""
149@@ -35,6 +35,7 @@
150 queryMultiAdapter,
151 )
152 from zope.formlib import form
153+from zope.formlib.widget import CustomWidgetFactory
154 from zope.formlib.widgets import DropdownWidget
155 from zope.schema import (
156 Bool,
157@@ -62,7 +63,6 @@
158 )
159 from lp.app.browser.launchpadform import (
160 action,
161- custom_widget,
162 LaunchpadFormView,
163 safe_action,
164 )
165@@ -174,11 +174,12 @@
166
167 schema = ISearchQuestionsForm
168
169- custom_widget('language', LabeledMultiCheckBoxWidget,
170- orientation='horizontal')
171- custom_widget('sort', DropdownWidget, cssClass='inlined-widget')
172- custom_widget('status', LabeledMultiCheckBoxWidget,
173- orientation='horizontal')
174+ custom_widget_language = CustomWidgetFactory(
175+ LabeledMultiCheckBoxWidget, orientation='horizontal')
176+ custom_widget_sort = CustomWidgetFactory(
177+ DropdownWidget, cssClass='inlined-widget')
178+ custom_widget_status = CustomWidgetFactory(
179+ LabeledMultiCheckBoxWidget, orientation='horizontal')
180
181 default_template = ViewPageTemplateFile(
182 '../templates/question-listing.pt')
183@@ -597,7 +598,8 @@
184 for the QuestionTarget context.
185 """
186
187- custom_widget('language', LabeledMultiCheckBoxWidget, visible=False)
188+ custom_widget_language = CustomWidgetFactory(
189+ LabeledMultiCheckBoxWidget, visible=False)
190
191 # No point showing a matching FAQs link on this report.
192 matching_faqs_count = 0
193@@ -672,7 +674,7 @@
194 return 'Answer contact for %s' % self.context.title
195
196 label = page_title
197- custom_widget('answer_contact_teams', LabeledMultiCheckBoxWidget)
198+ custom_widget_answer_contact_teams = LabeledMultiCheckBoxWidget
199
200 def setUpFields(self):
201 """See `LaunchpadFormView`."""
202
203=== modified file 'lib/lp/app/browser/doc/launchpadform-view.txt'
204--- lib/lp/app/browser/doc/launchpadform-view.txt 2018-03-28 19:31:02 +0000
205+++ lib/lp/app/browser/doc/launchpadform-view.txt 2018-08-09 15:10:30 +0000
206@@ -1,18 +1,18 @@
207-Launchpadform views
208+LaunchpadForm views
209 ===================
210
211-The custom_widget accepts arbitrary attribute assignments for the
212+CustomWidgetFactory accepts arbitrary attribute assignments for the
213 widget. One that launchpadform utilizes is 'widget_class'. The
214 widget rendering is wrapped with a <div> using the widget_class, which
215 can be used for subordinate field indentation, for example.
216
217+ >>> from zope.formlib.widget import CustomWidgetFactory
218 >>> from zope.formlib.widgets import TextWidget
219 >>> from zope.interface import Interface
220 >>> from zope.schema import TextLine
221 >>> from lp.services.config import config
222 >>> from z3c.ptcompat import ViewPageTemplateFile
223- >>> from lp.app.browser.launchpadform import (
224- ... custom_widget, LaunchpadFormView)
225+ >>> from lp.app.browser.launchpadform import LaunchpadFormView
226 >>> from lp.testing.pages import find_tags_by_class
227 >>> from lp.services.webapp.servers import LaunchpadTestRequest
228
229@@ -25,8 +25,8 @@
230 ... template = ViewPageTemplateFile(
231 ... config.root + '/lib/lp/app/templates/generic-edit.pt')
232 ... schema = ITestSchema
233- ... custom_widget('nickname', TextWidget,
234- ... widget_class="field subordinate")
235+ ... custom_widget_nickname = CustomWidgetFactory(
236+ ... TextWidget, widget_class='field subordinate')
237
238 >>> login('foo.bar@canonical.com')
239 >>> person = factory.makePerson()
240
241=== modified file 'lib/lp/app/browser/launchpad.py'
242--- lib/lp/app/browser/launchpad.py 2016-06-22 21:04:30 +0000
243+++ lib/lp/app/browser/launchpad.py 2018-08-09 15:10:30 +0000
244@@ -1,4 +1,4 @@
245-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
246+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
247 # GNU Affero General Public License version 3 (see the file LICENSE).
248
249 """Browser code for the launchpad application."""
250@@ -68,10 +68,7 @@
251 ExportedFolder,
252 ExportedImageFolder,
253 )
254-from lp.app.browser.launchpadform import (
255- custom_widget,
256- LaunchpadFormView,
257- )
258+from lp.app.browser.launchpadform import LaunchpadFormView
259 from lp.app.browser.tales import (
260 DurationFormatterAPI,
261 MenuAPI,
262@@ -1135,7 +1132,7 @@
263 class AppFrontPageSearchView(LaunchpadFormView):
264
265 schema = IAppFrontPageSearchForm
266- custom_widget('scope', ProjectScopeWidget)
267+ custom_widget_scope = ProjectScopeWidget
268
269 @property
270 def scope_css_class(self):
271
272=== modified file 'lib/lp/app/browser/launchpadform.py'
273--- lib/lp/app/browser/launchpadform.py 2015-07-08 16:05:11 +0000
274+++ lib/lp/app/browser/launchpadform.py 2018-08-09 15:10:30 +0000
275@@ -1,4 +1,4 @@
276-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
277+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
278 # GNU Affero General Public License version 3 (see the file LICENSE).
279
280 """Launchpad Form View Classes
281@@ -25,7 +25,10 @@
282 from zope.formlib import form
283 # imported so it may be exported
284 from zope.formlib.form import action
285-from zope.formlib.interfaces import IInputWidget
286+from zope.formlib.interfaces import (
287+ IInputWidget,
288+ IWidgetFactory,
289+ )
290 from zope.formlib.widget import CustomWidgetFactory
291 from zope.formlib.widgets import (
292 CheckBoxWidget,
293@@ -78,7 +81,7 @@
294 # Subset of fields to use
295 field_names = None
296 # Dictionary mapping field names to custom widgets
297- custom_widgets = ()
298+ custom_widgets = {}
299
300 # The next URL to redirect to on successful form submission
301 next_url = None
302@@ -197,12 +200,19 @@
303
304 If no context is given, the view's context is used."""
305 for field in self.form_fields:
306- if (field.custom_widget is None and
307- field.__name__ in self.custom_widgets):
308- # The check for custom_widget is None means that we honor the
309- # value if previously set. This is important for some existing
310- # forms.
311- field.custom_widget = self.custom_widgets[field.__name__]
312+ # Honour the custom_widget value if it was already set. This is
313+ # important for some existing forms.
314+ if field.custom_widget is None:
315+ widget = getattr(
316+ self, 'custom_widget_%s' % field.__name__, None)
317+ if widget is None:
318+ widget = self.custom_widgets.get(field.__name__)
319+ if widget is not None:
320+ if IWidgetFactory.providedBy(widget):
321+ field.custom_widget = widget
322+ else:
323+ # Allow views to save some typing in common cases.
324+ field.custom_widget = CustomWidgetFactory(widget)
325 if context is None:
326 context = self.context
327 self.widgets = form.setUpWidgets(
328
329=== modified file 'lib/lp/app/browser/multistep.py'
330--- lib/lp/app/browser/multistep.py 2013-04-10 08:09:05 +0000
331+++ lib/lp/app/browser/multistep.py 2018-08-09 15:10:30 +0000
332@@ -1,4 +1,4 @@
333-# Copyright 2009 Canonical Ltd. This software is licensed under the
334+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
335 # GNU Affero General Public License version 3 (see the file LICENSE).
336
337 """Multiple step views."""
338@@ -11,6 +11,7 @@
339
340
341 from zope.formlib import form
342+from zope.formlib.widget import CustomWidgetFactory
343 from zope.formlib.widgets import TextWidget
344 from zope.interface import Interface
345 from zope.schema import TextLine
346@@ -18,7 +19,6 @@
347 from lp import _
348 from lp.app.browser.launchpadform import (
349 action,
350- custom_widget,
351 LaunchpadFormView,
352 )
353 from lp.services.webapp import (
354@@ -148,7 +148,8 @@
355 override `main_action_label`.
356 """
357 # Use a custom widget in order to make it invisible.
358- custom_widget('__visited_steps__', TextWidget, visible=False)
359+ custom_widget___visited_steps__ = CustomWidgetFactory(
360+ TextWidget, visible=False)
361
362 _field_names = []
363 step_name = ''
364
365=== modified file 'lib/lp/app/browser/tests/test_launchpadform_doc.py'
366--- lib/lp/app/browser/tests/test_launchpadform_doc.py 2015-10-26 14:54:43 +0000
367+++ lib/lp/app/browser/tests/test_launchpadform_doc.py 2018-08-09 15:10:30 +0000
368@@ -1,4 +1,4 @@
369-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
370+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
371 # GNU Affero General Public License version 3 (see the file LICENSE).
372
373 import doctest
374@@ -95,14 +95,13 @@
375
376
377 def doctest_custom_widget_with_setUpFields_override():
378- """As a regression test, it is important to note that the custom_widget
379- class advisor should still work when setUpFields is overridden. For
380- instance, consider this custom widget and view:
381+ """As a regression test, it is important to note that custom widgets
382+ should still work when setUpFields is overridden. For instance,
383+ consider this custom widget and view:
384
385 >>> from zope.formlib.interfaces import IDisplayWidget, IInputWidget
386 >>> from zope.interface import directlyProvides, implements
387- >>> from lp.app.browser.launchpadform import (
388- ... LaunchpadFormView, custom_widget)
389+ >>> from lp.app.browser.launchpadform import LaunchpadFormView
390 >>> from zope.schema import Bool
391 >>> from zope.publisher.browser import TestRequest
392 >>> from zope.formlib import form
393@@ -121,7 +120,7 @@
394 ... self.value = value
395 ...
396 >>> class CustomView(LaunchpadFormView):
397- ... custom_widget('my_bool', CustomStubWidget)
398+ ... custom_widget_my_bool = CustomStubWidget
399 ... def setUpFields(self):
400 ... self.form_fields = form.Fields(Bool(__name__='my_bool'))
401 ...
402
403=== modified file 'lib/lp/app/doc/launchpadform.txt'
404--- lib/lp/app/doc/launchpadform.txt 2017-10-21 18:14:14 +0000
405+++ lib/lp/app/doc/launchpadform.txt 2018-08-09 15:10:30 +0000
406@@ -12,9 +12,8 @@
407 * if only a subset of the fields are to be displayed in the form, the
408 "field_names" attribute should be set.
409
410- * if any fields require custom widgets, the "custom_widgets"
411- attribute should be set to a dictionary mapping field names to
412- widget factories.
413+ * if any fields require custom widgets, the "custom_widget_NAME"
414+ attribute for each field NAME should be set to a widget factory.
415
416 * one or more actions must be provided by the form if it is to
417 support submission.
418@@ -127,15 +126,16 @@
419 == Custom Widgets ==
420
421 In some cases we will want to use a custom widget for a particular
422-field. These can be installed easily with the "custom_widgets"
423+field. These can be installed easily with a "custom_widget_NAME"
424 attribute:
425
426+ >>> from zope.formlib.widget import CustomWidgetFactory
427 >>> from zope.formlib.widgets import TextWidget
428- >>> from lp.app.browser.launchpadform import custom_widget
429
430 >>> class FormTestView3(LaunchpadFormView):
431 ... schema = IFormTest
432- ... custom_widget('displayname', TextWidget, displayWidth=50)
433+ ... custom_widget_displayname = CustomWidgetFactory(
434+ ... TextWidget, displayWidth=50)
435
436 >>> context = FormTest()
437 >>> request = LaunchpadTestRequest()
438@@ -462,7 +462,7 @@
439 Any widget can be hidden in a LaunchpadFormView while still having its
440 value POSTed with the values of the other (visible) ones. The widget's
441 visibility is controlled by its 'visible' attribute, which can be set
442-through a custom_widget() call.
443+using a custom widget.
444
445 First we'll create a fake pagetemplate which doesn't use Launchpad's main
446 template and thus is way simpler.
447@@ -496,7 +496,8 @@
448 using its hidden() method, which should return a hidden <input> tag.
449
450 >>> class TestWidgetVisibility2(TestWidgetVisibility):
451- ... custom_widget('displayname', TextWidget, visible=False)
452+ ... custom_widget_displayname = CustomWidgetFactory(
453+ ... TextWidget, visible=False)
454
455 >>> view = TestWidgetVisibility2(context, request)
456
457
458=== modified file 'lib/lp/app/widgets/doc/image-widget.txt'
459--- lib/lp/app/widgets/doc/image-widget.txt 2017-10-21 18:14:14 +0000
460+++ lib/lp/app/widgets/doc/image-widget.txt 2018-08-09 15:10:30 +0000
461@@ -18,7 +18,7 @@
462 Whenever you have a form in which you want to use the image widget, you
463 have to explicitly say whether you want to use its ADD_STYLE or
464 EDIT_STYLE incarnation, by passing an extra argument to the widget's
465-constructor (or to custom_widget(), if you're using it).
466+constructor (or to CustomWidgetFactory(), if you're using it).
467
468 Our policy is not to ask people to upload images when creating a record,
469 but instead to expose this as an edit form after the object is created.
470
471=== modified file 'lib/lp/blueprints/browser/specification.py'
472--- lib/lp/blueprints/browser/specification.py 2018-01-26 14:38:31 +0000
473+++ lib/lp/blueprints/browser/specification.py 2018-08-09 15:10:30 +0000
474@@ -62,6 +62,7 @@
475 from zope.event import notify
476 from zope.formlib import form
477 from zope.formlib.form import Fields
478+from zope.formlib.widget import CustomWidgetFactory
479 from zope.formlib.widgets import (
480 TextAreaWidget,
481 TextWidget,
482@@ -82,7 +83,6 @@
483 from lp.app.browser.launchpad import AppFrontPageSearchView
484 from lp.app.browser.launchpadform import (
485 action,
486- custom_widget,
487 LaunchpadEditFormView,
488 LaunchpadFormView,
489 safe_action,
490@@ -206,8 +206,8 @@
491 page_title = 'Register a blueprint in Launchpad'
492 label = "Register a new blueprint"
493
494- custom_widget('specurl', TextWidget, displayWidth=60)
495- custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
496+ custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
497+ custom_widget_information_type = LaunchpadRadioWidgetWithDescription
498
499 def append_info_type(self, fields):
500 """Append an InformationType field for creating a Specification.
501@@ -790,9 +790,9 @@
502 schema = SpecificationEditSchema
503 field_names = ['name', 'title', 'specurl', 'summary', 'whiteboard']
504 label = 'Edit specification'
505- custom_widget('summary', TextAreaWidget, height=5)
506- custom_widget('whiteboard', TextAreaWidget, height=10)
507- custom_widget('specurl', TextWidget, displayWidth=60)
508+ custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
509+ custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=10)
510+ custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
511
512 @property
513 def adapters(self):
514@@ -815,13 +815,14 @@
515 class SpecificationEditWhiteboardView(SpecificationEditView):
516 label = 'Edit specification status whiteboard'
517 field_names = ['whiteboard']
518- custom_widget('whiteboard', TextAreaWidget, height=15)
519+ custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=15)
520
521
522 class SpecificationEditWorkItemsView(SpecificationEditView):
523 label = 'Edit specification work items'
524 field_names = ['workitems_text']
525- custom_widget('workitems_text', TextAreaWidget, height=15)
526+ custom_widget_workitems_text = CustomWidgetFactory(
527+ TextAreaWidget, height=15)
528
529 @action(_('Change'), name='change')
530 def change_action(self, action, data):
531@@ -863,7 +864,7 @@
532
533 field_names = ['information_type']
534
535- custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
536+ custom_widget_information_type = LaunchpadRadioWidgetWithDescription
537
538 @property
539 def schema(self):
540@@ -913,7 +914,7 @@
541 schema = ISpecification
542 label = 'Target to a distribution series'
543 field_names = ['distroseries', 'whiteboard']
544- custom_widget('whiteboard', TextAreaWidget, height=5)
545+ custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
546
547 @property
548 def initial_values(self):
549
550=== modified file 'lib/lp/blueprints/browser/sprint.py'
551--- lib/lp/blueprints/browser/sprint.py 2017-04-10 11:17:52 +0000
552+++ lib/lp/blueprints/browser/sprint.py 2018-08-09 15:10:30 +0000
553@@ -1,4 +1,4 @@
554-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
555+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
556 # GNU Affero General Public License version 3 (see the file LICENSE).
557
558 """Sprint views."""
559@@ -30,13 +30,13 @@
560 from lazr.restful.utils import smartquote
561 import pytz
562 from zope.component import getUtility
563+from zope.formlib.widget import CustomWidgetFactory
564 from zope.formlib.widgets import TextAreaWidget
565 from zope.interface import implementer
566
567 from lp import _
568 from lp.app.browser.launchpadform import (
569 action,
570- custom_widget,
571 LaunchpadEditFormView,
572 LaunchpadFormView,
573 )
574@@ -261,10 +261,12 @@
575 'time_zone', 'time_starts', 'time_ends', 'is_physical',
576 'address',
577 ]
578- custom_widget('summary', TextAreaWidget, height=5)
579- custom_widget('time_starts', DateTimeWidget, display_zone=False)
580- custom_widget('time_ends', DateTimeWidget, display_zone=False)
581- custom_widget('address', TextAreaWidget, height=3)
582+ custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
583+ custom_widget_time_starts = CustomWidgetFactory(
584+ DateTimeWidget, display_zone=False)
585+ custom_widget_time_ends = CustomWidgetFactory(
586+ DateTimeWidget, display_zone=False)
587+ custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
588
589 sprint = None
590
591@@ -331,10 +333,12 @@
592 'time_zone', 'time_starts', 'time_ends', 'is_physical',
593 'address',
594 ]
595- custom_widget('summary', TextAreaWidget, height=5)
596- custom_widget('time_starts', DateTimeWidget, display_zone=False)
597- custom_widget('time_ends', DateTimeWidget, display_zone=False)
598- custom_widget('address', TextAreaWidget, height=3)
599+ custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
600+ custom_widget_time_starts = CustomWidgetFactory(
601+ DateTimeWidget, display_zone=False)
602+ custom_widget_time_ends = CustomWidgetFactory(
603+ DateTimeWidget, display_zone=False)
604+ custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
605
606 def setUpWidgets(self):
607 LaunchpadEditFormView.setUpWidgets(self)
608
609=== modified file 'lib/lp/blueprints/browser/sprintattendance.py'
610--- lib/lp/blueprints/browser/sprintattendance.py 2014-11-24 12:22:05 +0000
611+++ lib/lp/blueprints/browser/sprintattendance.py 2018-08-09 15:10:30 +0000
612@@ -1,4 +1,4 @@
613-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
614+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
615 # GNU Affero General Public License version 3 (see the file LICENSE).
616
617 """Views for SprintAttendance."""
618@@ -12,11 +12,11 @@
619 from datetime import timedelta
620
621 import pytz
622+from zope.formlib.widget import CustomWidgetFactory
623
624 from lp import _
625 from lp.app.browser.launchpadform import (
626 action,
627- custom_widget,
628 LaunchpadFormView,
629 )
630 from lp.app.widgets.date import DateTimeWidget
631@@ -28,10 +28,10 @@
632 class BaseSprintAttendanceAddView(LaunchpadFormView):
633
634 schema = ISprintAttendance
635- custom_widget('time_starts', DateTimeWidget)
636- custom_widget('time_ends', DateTimeWidget)
637- custom_widget(
638- 'is_physical', LaunchpadBooleanRadioWidget, orientation='vertical',
639+ custom_widget_time_starts = DateTimeWidget
640+ custom_widget_time_ends = DateTimeWidget
641+ custom_widget_is_physical = CustomWidgetFactory(
642+ LaunchpadBooleanRadioWidget, orientation='vertical',
643 true_label="Physically", false_label="Remotely", hint=None)
644
645 @property