← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/custom-widget-no-class-advice-1 into lp:launchpad.

Commit message:
Start removing Zope class advice from custom widget registration.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/custom-widget-no-class-advice-1/+merge/349631

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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/custom-widget-no-class-advice-1 into lp:launchpad.
=== modified file 'lib/lp/answers/browser/faqtarget.py'
--- lib/lp/answers/browser/faqtarget.py	2012-01-01 02:58:52 +0000
+++ lib/lp/answers/browser/faqtarget.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """`IFAQTarget` browser views."""
@@ -14,7 +14,6 @@
 from lp.answers.interfaces.faq import IFAQ
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadFormView,
     )
 from lp.app.errors import NotFoundError
@@ -45,7 +44,7 @@
     label = _('Create a new FAQ')
     field_names = ['title', 'keywords', 'content']
 
-    custom_widget('keywords', TokensTextWidget)
+    custom_widget_keywords = TokensTextWidget
 
     @property
     def page_title(self):

=== modified file 'lib/lp/answers/browser/question.py'
--- lib/lp/answers/browser/question.py	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/browser/question.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Question views."""
@@ -37,7 +37,10 @@
 from zope.component import getUtility
 from zope.event import notify
 from zope.formlib import form
-from zope.formlib.widget import renderElement
+from zope.formlib.widget import (
+    CustomWidgetFactory,
+    renderElement,
+    )
 from zope.formlib.widgets import (
     TextAreaWidget,
     TextWidget,
@@ -78,7 +81,6 @@
 from lp.answers.vocabulary import UsesAnswersDistributionVocabulary
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadEditFormView,
     LaunchpadFormView,
     safe_action,
@@ -274,7 +276,7 @@
     """View for the Answer Tracker index page."""
 
     schema = IAnswersFrontPageSearchForm
-    custom_widget('scope', ProjectScopeWidget)
+    custom_widget_scope = ProjectScopeWidget
 
     page_title = 'Launchpad Answers'
     label = 'Questions and Answers'
@@ -560,7 +562,8 @@
     # The fields displayed on the search page.
     search_field_names = ['language', 'title']
 
-    custom_widget('title', TextWidget, displayWidth=40, displayMaxWidth=250)
+    custom_widget_title = CustomWidgetFactory(
+        TextWidget, displayWidth=40, displayMaxWidth=250)
 
     search_template = ViewPageTemplateFile(
         '../templates/question-add-search.pt')
@@ -603,8 +606,9 @@
         else:
             fields = self.form_fields
         for field in fields:
-            if field.__name__ in self.custom_widgets:
-                field.custom_widget = self.custom_widgets[field.__name__]
+            widget = getattr(self, 'custom_widget_%s' % field.__name__, None)
+            if widget is not None:
+                field.custom_widget = widget
         return fields
 
     def setUpWidgets(self):
@@ -755,9 +759,9 @@
         "language", "title", "description", "target", "assignee",
         "whiteboard"]
 
-    custom_widget('title', TextWidget, displayWidth=40)
-    custom_widget('whiteboard', TextAreaWidget, height=5)
-    custom_widget('target', QuestionTargetWidget)
+    custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=40)
+    custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
+    custom_widget_target = QuestionTargetWidget
 
     @property
     def page_title(self):
@@ -1239,8 +1243,8 @@
 
     field_names = ['title', 'keywords', 'content']
 
-    custom_widget('keywords', TokensTextWidget)
-    custom_widget("message", TextAreaWidget, height=5)
+    custom_widget_keywords = TokensTextWidget
+    custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
 
     @property
     def initial_values(self):
@@ -1262,8 +1266,7 @@
             copy_field(IQuestionLinkFAQForm['message']))
         self.form_fields['message'].field.title = _(
             'Additional comment for question #%s' % self.context.id)
-        self.form_fields['message'].custom_widget = (
-            self.custom_widgets['message'])
+        self.form_fields['message'].custom_widget = self.custom_widget_message
 
     @action(_('Create and Link'), name='create_and_link')
     def create_and_link_action(self, action, data):
@@ -1418,9 +1421,9 @@
 
     schema = IQuestionLinkFAQForm
 
-    custom_widget('faq', SearchableFAQRadioWidget)
+    custom_widget_faq = SearchableFAQRadioWidget
 
-    custom_widget("message", TextAreaWidget, height=5)
+    custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
 
     label = _('Is this a FAQ?')
 

=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/browser/questiontarget.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """IQuestionTarget browser views."""
@@ -35,6 +35,7 @@
     queryMultiAdapter,
     )
 from zope.formlib import form
+from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import DropdownWidget
 from zope.schema import (
     Bool,
@@ -62,7 +63,6 @@
     )
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadFormView,
     safe_action,
     )
@@ -174,11 +174,12 @@
 
     schema = ISearchQuestionsForm
 
-    custom_widget('language', LabeledMultiCheckBoxWidget,
-                  orientation='horizontal')
-    custom_widget('sort', DropdownWidget, cssClass='inlined-widget')
-    custom_widget('status', LabeledMultiCheckBoxWidget,
-                  orientation='horizontal')
+    custom_widget_language = CustomWidgetFactory(
+        LabeledMultiCheckBoxWidget, orientation='horizontal')
+    custom_widget_sort = CustomWidgetFactory(
+        DropdownWidget, cssClass='inlined-widget')
+    custom_widget_status = CustomWidgetFactory(
+        LabeledMultiCheckBoxWidget, orientation='horizontal')
 
     default_template = ViewPageTemplateFile(
         '../templates/question-listing.pt')
@@ -597,7 +598,8 @@
      for the QuestionTarget context.
      """
 
-    custom_widget('language', LabeledMultiCheckBoxWidget, visible=False)
+    custom_widget_language = CustomWidgetFactory(
+        LabeledMultiCheckBoxWidget, visible=False)
 
     # No point showing a matching FAQs link on this report.
     matching_faqs_count = 0
@@ -672,7 +674,7 @@
         return 'Answer contact for %s' % self.context.title
 
     label = page_title
-    custom_widget('answer_contact_teams', LabeledMultiCheckBoxWidget)
+    custom_widget_answer_contact_teams = LabeledMultiCheckBoxWidget
 
     def setUpFields(self):
         """See `LaunchpadFormView`."""

=== modified file 'lib/lp/app/browser/doc/launchpadform-view.txt'
--- lib/lp/app/browser/doc/launchpadform-view.txt	2018-03-28 19:31:02 +0000
+++ lib/lp/app/browser/doc/launchpadform-view.txt	2018-07-15 17:24:03 +0000
@@ -1,18 +1,18 @@
-Launchpadform views
+LaunchpadForm views
 ===================
 
-The custom_widget accepts arbitrary attribute assignments for the
+CustomWidgetFactory accepts arbitrary attribute assignments for the
 widget.  One that launchpadform utilizes is 'widget_class'.  The
 widget rendering is wrapped with a <div> using the widget_class, which
 can be used for subordinate field indentation, for example.
 
+    >>> from zope.formlib.widget import CustomWidgetFactory
     >>> from zope.formlib.widgets import TextWidget
     >>> from zope.interface import Interface
     >>> from zope.schema import TextLine
     >>> from lp.services.config import config
     >>> from z3c.ptcompat import ViewPageTemplateFile
-    >>> from lp.app.browser.launchpadform import (
-    ...     custom_widget, LaunchpadFormView)
+    >>> from lp.app.browser.launchpadform import LaunchpadFormView
     >>> from lp.testing.pages import find_tags_by_class
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
@@ -25,8 +25,8 @@
     ...     template = ViewPageTemplateFile(
     ...         config.root + '/lib/lp/app/templates/generic-edit.pt')
     ...     schema = ITestSchema
-    ...     custom_widget('nickname', TextWidget,
-    ...                   widget_class="field subordinate")
+    ...     custom_widget_nickname = CustomWidgetFactory(
+    ...         TextWidget, widget_class='field subordinate')
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> person = factory.makePerson()

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2016-06-22 21:04:30 +0000
+++ lib/lp/app/browser/launchpad.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser code for the launchpad application."""
@@ -68,10 +68,7 @@
     ExportedFolder,
     ExportedImageFolder,
     )
-from lp.app.browser.launchpadform import (
-    custom_widget,
-    LaunchpadFormView,
-    )
+from lp.app.browser.launchpadform import LaunchpadFormView
 from lp.app.browser.tales import (
     DurationFormatterAPI,
     MenuAPI,
@@ -1135,7 +1132,7 @@
 class AppFrontPageSearchView(LaunchpadFormView):
 
     schema = IAppFrontPageSearchForm
-    custom_widget('scope', ProjectScopeWidget)
+    custom_widget_scope = ProjectScopeWidget
 
     @property
     def scope_css_class(self):

=== modified file 'lib/lp/app/browser/launchpadform.py'
--- lib/lp/app/browser/launchpadform.py	2015-07-08 16:05:11 +0000
+++ lib/lp/app/browser/launchpadform.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Launchpad Form View Classes
@@ -25,7 +25,10 @@
 from zope.formlib import form
 # imported so it may be exported
 from zope.formlib.form import action
-from zope.formlib.interfaces import IInputWidget
+from zope.formlib.interfaces import (
+    IInputWidget,
+    IWidgetFactory,
+    )
 from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import (
     CheckBoxWidget,
@@ -78,7 +81,7 @@
     # Subset of fields to use
     field_names = None
     # Dictionary mapping field names to custom widgets
-    custom_widgets = ()
+    custom_widgets = {}
 
     # The next URL to redirect to on successful form submission
     next_url = None
@@ -197,12 +200,20 @@
 
         If no context is given, the view's context is used."""
         for field in self.form_fields:
-            if (field.custom_widget is None and
-                field.__name__ in self.custom_widgets):
-                # The check for custom_widget is None means that we honor the
-                # value if previously set. This is important for some existing
-                # forms.
-                field.custom_widget = self.custom_widgets[field.__name__]
+            # Honour the custom_widget value if it was already set.  This is
+            # important for some existing forms.
+            if field.custom_widget is None:
+                widget = getattr(
+                    self, 'custom_widget_%s' % field.__name__, None)
+                if widget is None:
+                    widget = self.custom_widgets.get(field.__name__)
+                if widget is not None:
+                    field.custom_widget = widget
+                    if IWidgetFactory.providedBy(widget):
+                        field.custom_widget = widget
+                    else:
+                        # Allow views to save some typing in common cases.
+                        field.custom_widget = CustomWidgetFactory(widget)
         if context is None:
             context = self.context
         self.widgets = form.setUpWidgets(

=== modified file 'lib/lp/app/browser/multistep.py'
--- lib/lp/app/browser/multistep.py	2013-04-10 08:09:05 +0000
+++ lib/lp/app/browser/multistep.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Multiple step views."""
@@ -11,6 +11,7 @@
 
 
 from zope.formlib import form
+from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import TextWidget
 from zope.interface import Interface
 from zope.schema import TextLine
@@ -18,7 +19,6 @@
 from lp import _
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadFormView,
     )
 from lp.services.webapp import (
@@ -148,7 +148,8 @@
     override `main_action_label`.
     """
     # Use a custom widget in order to make it invisible.
-    custom_widget('__visited_steps__', TextWidget, visible=False)
+    custom_widget___visited_steps__ = CustomWidgetFactory(
+        TextWidget, visible=False)
 
     _field_names = []
     step_name = ''

=== modified file 'lib/lp/app/browser/tests/test_launchpadform_doc.py'
--- lib/lp/app/browser/tests/test_launchpadform_doc.py	2015-10-26 14:54:43 +0000
+++ lib/lp/app/browser/tests/test_launchpadform_doc.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import doctest
@@ -95,14 +95,13 @@
 
 
 def doctest_custom_widget_with_setUpFields_override():
-    """As a regression test, it is important to note that the custom_widget
-    class advisor should still work when setUpFields is overridden.  For
-    instance, consider this custom widget and view:
+    """As a regression test, it is important to note that custom widgets
+    should still work when setUpFields is overridden.  For instance,
+    consider this custom widget and view:
 
         >>> from zope.formlib.interfaces import IDisplayWidget, IInputWidget
         >>> from zope.interface import directlyProvides, implements
-        >>> from lp.app.browser.launchpadform import (
-        ...     LaunchpadFormView, custom_widget)
+        >>> from lp.app.browser.launchpadform import LaunchpadFormView
         >>> from zope.schema import Bool
         >>> from zope.publisher.browser import TestRequest
         >>> from zope.formlib import form
@@ -121,7 +120,7 @@
         ...         self.value = value
         ...
         >>> class CustomView(LaunchpadFormView):
-        ...     custom_widget('my_bool', CustomStubWidget)
+        ...     custom_widget_my_bool = CustomStubWidget
         ...     def setUpFields(self):
         ...         self.form_fields = form.Fields(Bool(__name__='my_bool'))
         ...

=== modified file 'lib/lp/app/doc/launchpadform.txt'
--- lib/lp/app/doc/launchpadform.txt	2017-10-21 18:14:14 +0000
+++ lib/lp/app/doc/launchpadform.txt	2018-07-15 17:24:03 +0000
@@ -12,9 +12,8 @@
  * if only a subset of the fields are to be displayed in the form, the
    "field_names" attribute should be set.
 
- * if any fields require custom widgets, the "custom_widgets"
-   attribute should be set to a dictionary mapping field names to
-   widget factories.
+ * if any fields require custom widgets, the "custom_widget_NAME"
+   attribute for each field NAME should be set to a widget factory.
 
  * one or more actions must be provided by the form if it is to
    support submission.
@@ -127,15 +126,16 @@
 == Custom Widgets ==
 
 In some cases we will want to use a custom widget for a particular
-field.  These can be installed easily with the "custom_widgets"
+field.  These can be installed easily with a "custom_widget_NAME"
 attribute:
 
+  >>> from zope.formlib.widget import CustomWidgetFactory
   >>> from zope.formlib.widgets import TextWidget
-  >>> from lp.app.browser.launchpadform import custom_widget
 
   >>> class FormTestView3(LaunchpadFormView):
   ...     schema = IFormTest
-  ...     custom_widget('displayname', TextWidget, displayWidth=50)
+  ...     custom_widget_displayname = CustomWidgetFactory(
+  ...         TextWidget, displayWidth=50)
 
   >>> context = FormTest()
   >>> request = LaunchpadTestRequest()
@@ -462,7 +462,7 @@
 Any widget can be hidden in a LaunchpadFormView while still having its
 value POSTed with the values of the other (visible) ones. The widget's
 visibility is controlled by its 'visible' attribute, which can be set
-through a custom_widget() call.
+using a custom widget.
 
 First we'll create a fake pagetemplate which doesn't use Launchpad's main
 template and thus is way simpler.
@@ -496,7 +496,8 @@
 using its hidden() method, which should return a hidden <input> tag.
 
   >>> class TestWidgetVisibility2(TestWidgetVisibility):
-  ...     custom_widget('displayname', TextWidget, visible=False)
+  ...     custom_widget_displayname = CustomWidgetFactory(
+  ...         TextWidget, visible=False)
 
   >>> view = TestWidgetVisibility2(context, request)
 

=== modified file 'lib/lp/app/widgets/doc/image-widget.txt'
--- lib/lp/app/widgets/doc/image-widget.txt	2017-10-21 18:14:14 +0000
+++ lib/lp/app/widgets/doc/image-widget.txt	2018-07-15 17:24:03 +0000
@@ -18,7 +18,7 @@
 Whenever you have a form in which you want to use the image widget, you
 have to explicitly say whether you want to use its ADD_STYLE or
 EDIT_STYLE incarnation, by passing an extra argument to the widget's
-constructor (or to custom_widget(), if you're using it).
+constructor (or to CustomWidgetFactory(), if you're using it).
 
 Our policy is not to ask people to upload images when creating a record,
 but instead to expose this as an edit form after the object is created.

=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py	2018-01-26 14:38:31 +0000
+++ lib/lp/blueprints/browser/specification.py	2018-07-15 17:24:03 +0000
@@ -62,6 +62,7 @@
 from zope.event import notify
 from zope.formlib import form
 from zope.formlib.form import Fields
+from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import (
     TextAreaWidget,
     TextWidget,
@@ -82,7 +83,6 @@
 from lp.app.browser.launchpad import AppFrontPageSearchView
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadEditFormView,
     LaunchpadFormView,
     safe_action,
@@ -206,8 +206,8 @@
     page_title = 'Register a blueprint in Launchpad'
     label = "Register a new blueprint"
 
-    custom_widget('specurl', TextWidget, displayWidth=60)
-    custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+    custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
+    custom_widget_information_type = LaunchpadRadioWidgetWithDescription
 
     def append_info_type(self, fields):
         """Append an InformationType field for creating a Specification.
@@ -790,9 +790,9 @@
     schema = SpecificationEditSchema
     field_names = ['name', 'title', 'specurl', 'summary', 'whiteboard']
     label = 'Edit specification'
-    custom_widget('summary', TextAreaWidget, height=5)
-    custom_widget('whiteboard', TextAreaWidget, height=10)
-    custom_widget('specurl', TextWidget, displayWidth=60)
+    custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
+    custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=10)
+    custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
 
     @property
     def adapters(self):
@@ -815,13 +815,14 @@
 class SpecificationEditWhiteboardView(SpecificationEditView):
     label = 'Edit specification status whiteboard'
     field_names = ['whiteboard']
-    custom_widget('whiteboard', TextAreaWidget, height=15)
+    custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=15)
 
 
 class SpecificationEditWorkItemsView(SpecificationEditView):
     label = 'Edit specification work items'
     field_names = ['workitems_text']
-    custom_widget('workitems_text', TextAreaWidget, height=15)
+    custom_widget_workitems_text = CustomWidgetFactory(
+        TextAreaWidget, height=15)
 
     @action(_('Change'), name='change')
     def change_action(self, action, data):
@@ -863,7 +864,7 @@
 
     field_names = ['information_type']
 
-    custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+    custom_widget_information_type = LaunchpadRadioWidgetWithDescription
 
     @property
     def schema(self):
@@ -913,7 +914,7 @@
     schema = ISpecification
     label = 'Target to a distribution series'
     field_names = ['distroseries', 'whiteboard']
-    custom_widget('whiteboard', TextAreaWidget, height=5)
+    custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
 
     @property
     def initial_values(self):

=== modified file 'lib/lp/blueprints/browser/sprint.py'
--- lib/lp/blueprints/browser/sprint.py	2017-04-10 11:17:52 +0000
+++ lib/lp/blueprints/browser/sprint.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Sprint views."""
@@ -30,13 +30,13 @@
 from lazr.restful.utils import smartquote
 import pytz
 from zope.component import getUtility
+from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import TextAreaWidget
 from zope.interface import implementer
 
 from lp import _
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadEditFormView,
     LaunchpadFormView,
     )
@@ -261,10 +261,12 @@
                    'time_zone', 'time_starts', 'time_ends', 'is_physical',
                    'address',
                    ]
-    custom_widget('summary', TextAreaWidget, height=5)
-    custom_widget('time_starts', DateTimeWidget, display_zone=False)
-    custom_widget('time_ends', DateTimeWidget, display_zone=False)
-    custom_widget('address', TextAreaWidget, height=3)
+    custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
+    custom_widget_time_starts = CustomWidgetFactory(
+        DateTimeWidget, display_zone=False)
+    custom_widget_time_ends = CustomWidgetFactory(
+        DateTimeWidget, display_zone=False)
+    custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
 
     sprint = None
 
@@ -331,10 +333,12 @@
                    'time_zone', 'time_starts', 'time_ends', 'is_physical',
                    'address',
                    ]
-    custom_widget('summary', TextAreaWidget, height=5)
-    custom_widget('time_starts', DateTimeWidget, display_zone=False)
-    custom_widget('time_ends', DateTimeWidget, display_zone=False)
-    custom_widget('address', TextAreaWidget, height=3)
+    custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
+    custom_widget_time_starts = CustomWidgetFactory(
+        DateTimeWidget, display_zone=False)
+    custom_widget_time_ends = CustomWidgetFactory(
+        DateTimeWidget, display_zone=False)
+    custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
 
     def setUpWidgets(self):
         LaunchpadEditFormView.setUpWidgets(self)

=== modified file 'lib/lp/blueprints/browser/sprintattendance.py'
--- lib/lp/blueprints/browser/sprintattendance.py	2014-11-24 12:22:05 +0000
+++ lib/lp/blueprints/browser/sprintattendance.py	2018-07-15 17:24:03 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Views for SprintAttendance."""
@@ -12,11 +12,11 @@
 from datetime import timedelta
 
 import pytz
+from zope.formlib.widget import CustomWidgetFactory
 
 from lp import _
 from lp.app.browser.launchpadform import (
     action,
-    custom_widget,
     LaunchpadFormView,
     )
 from lp.app.widgets.date import DateTimeWidget
@@ -28,10 +28,10 @@
 class BaseSprintAttendanceAddView(LaunchpadFormView):
 
     schema = ISprintAttendance
-    custom_widget('time_starts', DateTimeWidget)
-    custom_widget('time_ends', DateTimeWidget)
-    custom_widget(
-        'is_physical', LaunchpadBooleanRadioWidget, orientation='vertical',
+    custom_widget_time_starts = DateTimeWidget
+    custom_widget_time_ends = DateTimeWidget
+    custom_widget_is_physical = CustomWidgetFactory(
+        LaunchpadBooleanRadioWidget, orientation='vertical',
         true_label="Physically", false_label="Remotely", hint=None)
 
     @property


Follow ups