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

Proposed by Colin Watson
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 Approve
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.
Revision history for this message
William Grant (wgrant) wrote :

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

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""`IFAQTarget` browser views."""4"""`IFAQTarget` browser views."""
@@ -14,7 +14,6 @@
14from lp.answers.interfaces.faq import IFAQ14from lp.answers.interfaces.faq import IFAQ
15from lp.app.browser.launchpadform import (15from lp.app.browser.launchpadform import (
16 action,16 action,
17 custom_widget,
18 LaunchpadFormView,17 LaunchpadFormView,
19 )18 )
20from lp.app.errors import NotFoundError19from lp.app.errors import NotFoundError
@@ -45,7 +44,7 @@
45 label = _('Create a new FAQ')44 label = _('Create a new FAQ')
46 field_names = ['title', 'keywords', 'content']45 field_names = ['title', 'keywords', 'content']
4746
48 custom_widget('keywords', TokensTextWidget)47 custom_widget_keywords = TokensTextWidget
4948
50 @property49 @property
51 def page_title(self):50 def page_title(self):
5251
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Question views."""4"""Question views."""
@@ -37,7 +37,11 @@
37from zope.component import getUtility37from zope.component import getUtility
38from zope.event import notify38from zope.event import notify
39from zope.formlib import form39from zope.formlib import form
40from zope.formlib.widget import renderElement40from zope.formlib.interfaces import IWidgetFactory
41from zope.formlib.widget import (
42 CustomWidgetFactory,
43 renderElement,
44 )
41from zope.formlib.widgets import (45from zope.formlib.widgets import (
42 TextAreaWidget,46 TextAreaWidget,
43 TextWidget,47 TextWidget,
@@ -78,7 +82,6 @@
78from lp.answers.vocabulary import UsesAnswersDistributionVocabulary82from lp.answers.vocabulary import UsesAnswersDistributionVocabulary
79from lp.app.browser.launchpadform import (83from lp.app.browser.launchpadform import (
80 action,84 action,
81 custom_widget,
82 LaunchpadEditFormView,85 LaunchpadEditFormView,
83 LaunchpadFormView,86 LaunchpadFormView,
84 safe_action,87 safe_action,
@@ -274,7 +277,7 @@
274 """View for the Answer Tracker index page."""277 """View for the Answer Tracker index page."""
275278
276 schema = IAnswersFrontPageSearchForm279 schema = IAnswersFrontPageSearchForm
277 custom_widget('scope', ProjectScopeWidget)280 custom_widget_scope = ProjectScopeWidget
278281
279 page_title = 'Launchpad Answers'282 page_title = 'Launchpad Answers'
280 label = 'Questions and Answers'283 label = 'Questions and Answers'
@@ -560,7 +563,8 @@
560 # The fields displayed on the search page.563 # The fields displayed on the search page.
561 search_field_names = ['language', 'title']564 search_field_names = ['language', 'title']
562565
563 custom_widget('title', TextWidget, displayWidth=40, displayMaxWidth=250)566 custom_widget_title = CustomWidgetFactory(
567 TextWidget, displayWidth=40, displayMaxWidth=250)
564568
565 search_template = ViewPageTemplateFile(569 search_template = ViewPageTemplateFile(
566 '../templates/question-add-search.pt')570 '../templates/question-add-search.pt')
@@ -603,8 +607,13 @@
603 else:607 else:
604 fields = self.form_fields608 fields = self.form_fields
605 for field in fields:609 for field in fields:
606 if field.__name__ in self.custom_widgets:610 widget = getattr(self, 'custom_widget_%s' % field.__name__, None)
607 field.custom_widget = self.custom_widgets[field.__name__]611 if widget is not None:
612 if IWidgetFactory.providedBy(widget):
613 field.custom_widget = widget
614 else:
615 # Allow views to save some typing in common cases.
616 field.custom_widget = CustomWidgetFactory(widget)
608 return fields617 return fields
609618
610 def setUpWidgets(self):619 def setUpWidgets(self):
@@ -755,9 +764,9 @@
755 "language", "title", "description", "target", "assignee",764 "language", "title", "description", "target", "assignee",
756 "whiteboard"]765 "whiteboard"]
757766
758 custom_widget('title', TextWidget, displayWidth=40)767 custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=40)
759 custom_widget('whiteboard', TextAreaWidget, height=5)768 custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
760 custom_widget('target', QuestionTargetWidget)769 custom_widget_target = QuestionTargetWidget
761770
762 @property771 @property
763 def page_title(self):772 def page_title(self):
@@ -1239,8 +1248,8 @@
12391248
1240 field_names = ['title', 'keywords', 'content']1249 field_names = ['title', 'keywords', 'content']
12411250
1242 custom_widget('keywords', TokensTextWidget)1251 custom_widget_keywords = TokensTextWidget
1243 custom_widget("message", TextAreaWidget, height=5)1252 custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
12441253
1245 @property1254 @property
1246 def initial_values(self):1255 def initial_values(self):
@@ -1262,8 +1271,7 @@
1262 copy_field(IQuestionLinkFAQForm['message']))1271 copy_field(IQuestionLinkFAQForm['message']))
1263 self.form_fields['message'].field.title = _(1272 self.form_fields['message'].field.title = _(
1264 'Additional comment for question #%s' % self.context.id)1273 'Additional comment for question #%s' % self.context.id)
1265 self.form_fields['message'].custom_widget = (1274 self.form_fields['message'].custom_widget = self.custom_widget_message
1266 self.custom_widgets['message'])
12671275
1268 @action(_('Create and Link'), name='create_and_link')1276 @action(_('Create and Link'), name='create_and_link')
1269 def create_and_link_action(self, action, data):1277 def create_and_link_action(self, action, data):
@@ -1418,9 +1426,9 @@
14181426
1419 schema = IQuestionLinkFAQForm1427 schema = IQuestionLinkFAQForm
14201428
1421 custom_widget('faq', SearchableFAQRadioWidget)1429 custom_widget_faq = SearchableFAQRadioWidget
14221430
1423 custom_widget("message", TextAreaWidget, height=5)1431 custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
14241432
1425 label = _('Is this a FAQ?')1433 label = _('Is this a FAQ?')
14261434
14271435
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""IQuestionTarget browser views."""4"""IQuestionTarget browser views."""
@@ -35,6 +35,7 @@
35 queryMultiAdapter,35 queryMultiAdapter,
36 )36 )
37from zope.formlib import form37from zope.formlib import form
38from zope.formlib.widget import CustomWidgetFactory
38from zope.formlib.widgets import DropdownWidget39from zope.formlib.widgets import DropdownWidget
39from zope.schema import (40from zope.schema import (
40 Bool,41 Bool,
@@ -62,7 +63,6 @@
62 )63 )
63from lp.app.browser.launchpadform import (64from lp.app.browser.launchpadform import (
64 action,65 action,
65 custom_widget,
66 LaunchpadFormView,66 LaunchpadFormView,
67 safe_action,67 safe_action,
68 )68 )
@@ -174,11 +174,12 @@
174174
175 schema = ISearchQuestionsForm175 schema = ISearchQuestionsForm
176176
177 custom_widget('language', LabeledMultiCheckBoxWidget,177 custom_widget_language = CustomWidgetFactory(
178 orientation='horizontal')178 LabeledMultiCheckBoxWidget, orientation='horizontal')
179 custom_widget('sort', DropdownWidget, cssClass='inlined-widget')179 custom_widget_sort = CustomWidgetFactory(
180 custom_widget('status', LabeledMultiCheckBoxWidget,180 DropdownWidget, cssClass='inlined-widget')
181 orientation='horizontal')181 custom_widget_status = CustomWidgetFactory(
182 LabeledMultiCheckBoxWidget, orientation='horizontal')
182183
183 default_template = ViewPageTemplateFile(184 default_template = ViewPageTemplateFile(
184 '../templates/question-listing.pt')185 '../templates/question-listing.pt')
@@ -597,7 +598,8 @@
597 for the QuestionTarget context.598 for the QuestionTarget context.
598 """599 """
599600
600 custom_widget('language', LabeledMultiCheckBoxWidget, visible=False)601 custom_widget_language = CustomWidgetFactory(
602 LabeledMultiCheckBoxWidget, visible=False)
601603
602 # No point showing a matching FAQs link on this report.604 # No point showing a matching FAQs link on this report.
603 matching_faqs_count = 0605 matching_faqs_count = 0
@@ -672,7 +674,7 @@
672 return 'Answer contact for %s' % self.context.title674 return 'Answer contact for %s' % self.context.title
673675
674 label = page_title676 label = page_title
675 custom_widget('answer_contact_teams', LabeledMultiCheckBoxWidget)677 custom_widget_answer_contact_teams = LabeledMultiCheckBoxWidget
676678
677 def setUpFields(self):679 def setUpFields(self):
678 """See `LaunchpadFormView`."""680 """See `LaunchpadFormView`."""
679681
=== 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-08-09 15:10:30 +0000
@@ -1,18 +1,18 @@
1Launchpadform views1LaunchpadForm views
2===================2===================
33
4The custom_widget accepts arbitrary attribute assignments for the4CustomWidgetFactory accepts arbitrary attribute assignments for the
5widget. One that launchpadform utilizes is 'widget_class'. The5widget. One that launchpadform utilizes is 'widget_class'. The
6widget rendering is wrapped with a <div> using the widget_class, which6widget rendering is wrapped with a <div> using the widget_class, which
7can be used for subordinate field indentation, for example.7can be used for subordinate field indentation, for example.
88
9 >>> from zope.formlib.widget import CustomWidgetFactory
9 >>> from zope.formlib.widgets import TextWidget10 >>> from zope.formlib.widgets import TextWidget
10 >>> from zope.interface import Interface11 >>> from zope.interface import Interface
11 >>> from zope.schema import TextLine12 >>> from zope.schema import TextLine
12 >>> from lp.services.config import config13 >>> from lp.services.config import config
13 >>> from z3c.ptcompat import ViewPageTemplateFile14 >>> from z3c.ptcompat import ViewPageTemplateFile
14 >>> from lp.app.browser.launchpadform import (15 >>> from lp.app.browser.launchpadform import LaunchpadFormView
15 ... custom_widget, LaunchpadFormView)
16 >>> from lp.testing.pages import find_tags_by_class16 >>> from lp.testing.pages import find_tags_by_class
17 >>> from lp.services.webapp.servers import LaunchpadTestRequest17 >>> from lp.services.webapp.servers import LaunchpadTestRequest
1818
@@ -25,8 +25,8 @@
25 ... template = ViewPageTemplateFile(25 ... template = ViewPageTemplateFile(
26 ... config.root + '/lib/lp/app/templates/generic-edit.pt')26 ... config.root + '/lib/lp/app/templates/generic-edit.pt')
27 ... schema = ITestSchema27 ... schema = ITestSchema
28 ... custom_widget('nickname', TextWidget,28 ... custom_widget_nickname = CustomWidgetFactory(
29 ... widget_class="field subordinate")29 ... TextWidget, widget_class='field subordinate')
3030
31 >>> login('foo.bar@canonical.com')31 >>> login('foo.bar@canonical.com')
32 >>> person = factory.makePerson()32 >>> person = factory.makePerson()
3333
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Browser code for the launchpad application."""4"""Browser code for the launchpad application."""
@@ -68,10 +68,7 @@
68 ExportedFolder,68 ExportedFolder,
69 ExportedImageFolder,69 ExportedImageFolder,
70 )70 )
71from lp.app.browser.launchpadform import (71from lp.app.browser.launchpadform import LaunchpadFormView
72 custom_widget,
73 LaunchpadFormView,
74 )
75from lp.app.browser.tales import (72from lp.app.browser.tales import (
76 DurationFormatterAPI,73 DurationFormatterAPI,
77 MenuAPI,74 MenuAPI,
@@ -1135,7 +1132,7 @@
1135class AppFrontPageSearchView(LaunchpadFormView):1132class AppFrontPageSearchView(LaunchpadFormView):
11361133
1137 schema = IAppFrontPageSearchForm1134 schema = IAppFrontPageSearchForm
1138 custom_widget('scope', ProjectScopeWidget)1135 custom_widget_scope = ProjectScopeWidget
11391136
1140 @property1137 @property
1141 def scope_css_class(self):1138 def scope_css_class(self):
11421139
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Launchpad Form View Classes4"""Launchpad Form View Classes
@@ -25,7 +25,10 @@
25from zope.formlib import form25from zope.formlib import form
26# imported so it may be exported26# imported so it may be exported
27from zope.formlib.form import action27from zope.formlib.form import action
28from zope.formlib.interfaces import IInputWidget28from zope.formlib.interfaces import (
29 IInputWidget,
30 IWidgetFactory,
31 )
29from zope.formlib.widget import CustomWidgetFactory32from zope.formlib.widget import CustomWidgetFactory
30from zope.formlib.widgets import (33from zope.formlib.widgets import (
31 CheckBoxWidget,34 CheckBoxWidget,
@@ -78,7 +81,7 @@
78 # Subset of fields to use81 # Subset of fields to use
79 field_names = None82 field_names = None
80 # Dictionary mapping field names to custom widgets83 # Dictionary mapping field names to custom widgets
81 custom_widgets = ()84 custom_widgets = {}
8285
83 # The next URL to redirect to on successful form submission86 # The next URL to redirect to on successful form submission
84 next_url = None87 next_url = None
@@ -197,12 +200,19 @@
197200
198 If no context is given, the view's context is used."""201 If no context is given, the view's context is used."""
199 for field in self.form_fields:202 for field in self.form_fields:
200 if (field.custom_widget is None and203 # Honour the custom_widget value if it was already set. This is
201 field.__name__ in self.custom_widgets):204 # important for some existing forms.
202 # The check for custom_widget is None means that we honor the205 if field.custom_widget is None:
203 # value if previously set. This is important for some existing206 widget = getattr(
204 # forms.207 self, 'custom_widget_%s' % field.__name__, None)
205 field.custom_widget = self.custom_widgets[field.__name__]208 if widget is None:
209 widget = self.custom_widgets.get(field.__name__)
210 if widget is not None:
211 if IWidgetFactory.providedBy(widget):
212 field.custom_widget = widget
213 else:
214 # Allow views to save some typing in common cases.
215 field.custom_widget = CustomWidgetFactory(widget)
206 if context is None:216 if context is None:
207 context = self.context217 context = self.context
208 self.widgets = form.setUpWidgets(218 self.widgets = form.setUpWidgets(
209219
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Multiple step views."""4"""Multiple step views."""
@@ -11,6 +11,7 @@
1111
1212
13from zope.formlib import form13from zope.formlib import form
14from zope.formlib.widget import CustomWidgetFactory
14from zope.formlib.widgets import TextWidget15from zope.formlib.widgets import TextWidget
15from zope.interface import Interface16from zope.interface import Interface
16from zope.schema import TextLine17from zope.schema import TextLine
@@ -18,7 +19,6 @@
18from lp import _19from lp import _
19from lp.app.browser.launchpadform import (20from lp.app.browser.launchpadform import (
20 action,21 action,
21 custom_widget,
22 LaunchpadFormView,22 LaunchpadFormView,
23 )23 )
24from lp.services.webapp import (24from lp.services.webapp import (
@@ -148,7 +148,8 @@
148 override `main_action_label`.148 override `main_action_label`.
149 """149 """
150 # Use a custom widget in order to make it invisible.150 # Use a custom widget in order to make it invisible.
151 custom_widget('__visited_steps__', TextWidget, visible=False)151 custom_widget___visited_steps__ = CustomWidgetFactory(
152 TextWidget, visible=False)
152153
153 _field_names = []154 _field_names = []
154 step_name = ''155 step_name = ''
155156
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import doctest4import doctest
@@ -95,14 +95,13 @@
9595
9696
97def doctest_custom_widget_with_setUpFields_override():97def doctest_custom_widget_with_setUpFields_override():
98 """As a regression test, it is important to note that the custom_widget98 """As a regression test, it is important to note that custom widgets
99 class advisor should still work when setUpFields is overridden. For99 should still work when setUpFields is overridden. For instance,
100 instance, consider this custom widget and view:100 consider this custom widget and view:
101101
102 >>> from zope.formlib.interfaces import IDisplayWidget, IInputWidget102 >>> from zope.formlib.interfaces import IDisplayWidget, IInputWidget
103 >>> from zope.interface import directlyProvides, implements103 >>> from zope.interface import directlyProvides, implements
104 >>> from lp.app.browser.launchpadform import (104 >>> from lp.app.browser.launchpadform import LaunchpadFormView
105 ... LaunchpadFormView, custom_widget)
106 >>> from zope.schema import Bool105 >>> from zope.schema import Bool
107 >>> from zope.publisher.browser import TestRequest106 >>> from zope.publisher.browser import TestRequest
108 >>> from zope.formlib import form107 >>> from zope.formlib import form
@@ -121,7 +120,7 @@
121 ... self.value = value120 ... self.value = value
122 ...121 ...
123 >>> class CustomView(LaunchpadFormView):122 >>> class CustomView(LaunchpadFormView):
124 ... custom_widget('my_bool', CustomStubWidget)123 ... custom_widget_my_bool = CustomStubWidget
125 ... def setUpFields(self):124 ... def setUpFields(self):
126 ... self.form_fields = form.Fields(Bool(__name__='my_bool'))125 ... self.form_fields = form.Fields(Bool(__name__='my_bool'))
127 ...126 ...
128127
=== 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-08-09 15:10:30 +0000
@@ -12,9 +12,8 @@
12 * if only a subset of the fields are to be displayed in the form, the12 * if only a subset of the fields are to be displayed in the form, the
13 "field_names" attribute should be set.13 "field_names" attribute should be set.
1414
15 * if any fields require custom widgets, the "custom_widgets"15 * if any fields require custom widgets, the "custom_widget_NAME"
16 attribute should be set to a dictionary mapping field names to16 attribute for each field NAME should be set to a widget factory.
17 widget factories.
1817
19 * one or more actions must be provided by the form if it is to18 * one or more actions must be provided by the form if it is to
20 support submission.19 support submission.
@@ -127,15 +126,16 @@
127== Custom Widgets ==126== Custom Widgets ==
128127
129In some cases we will want to use a custom widget for a particular128In some cases we will want to use a custom widget for a particular
130field. These can be installed easily with the "custom_widgets"129field. These can be installed easily with a "custom_widget_NAME"
131attribute:130attribute:
132131
132 >>> from zope.formlib.widget import CustomWidgetFactory
133 >>> from zope.formlib.widgets import TextWidget133 >>> from zope.formlib.widgets import TextWidget
134 >>> from lp.app.browser.launchpadform import custom_widget
135134
136 >>> class FormTestView3(LaunchpadFormView):135 >>> class FormTestView3(LaunchpadFormView):
137 ... schema = IFormTest136 ... schema = IFormTest
138 ... custom_widget('displayname', TextWidget, displayWidth=50)137 ... custom_widget_displayname = CustomWidgetFactory(
138 ... TextWidget, displayWidth=50)
139139
140 >>> context = FormTest()140 >>> context = FormTest()
141 >>> request = LaunchpadTestRequest()141 >>> request = LaunchpadTestRequest()
@@ -462,7 +462,7 @@
462Any widget can be hidden in a LaunchpadFormView while still having its462Any widget can be hidden in a LaunchpadFormView while still having its
463value POSTed with the values of the other (visible) ones. The widget's463value POSTed with the values of the other (visible) ones. The widget's
464visibility is controlled by its 'visible' attribute, which can be set464visibility is controlled by its 'visible' attribute, which can be set
465through a custom_widget() call.465using a custom widget.
466466
467First we'll create a fake pagetemplate which doesn't use Launchpad's main467First we'll create a fake pagetemplate which doesn't use Launchpad's main
468template and thus is way simpler.468template and thus is way simpler.
@@ -496,7 +496,8 @@
496using its hidden() method, which should return a hidden <input> tag.496using its hidden() method, which should return a hidden <input> tag.
497497
498 >>> class TestWidgetVisibility2(TestWidgetVisibility):498 >>> class TestWidgetVisibility2(TestWidgetVisibility):
499 ... custom_widget('displayname', TextWidget, visible=False)499 ... custom_widget_displayname = CustomWidgetFactory(
500 ... TextWidget, visible=False)
500501
501 >>> view = TestWidgetVisibility2(context, request)502 >>> view = TestWidgetVisibility2(context, request)
502503
503504
=== 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-08-09 15:10:30 +0000
@@ -18,7 +18,7 @@
18Whenever you have a form in which you want to use the image widget, you18Whenever you have a form in which you want to use the image widget, you
19have to explicitly say whether you want to use its ADD_STYLE or19have to explicitly say whether you want to use its ADD_STYLE or
20EDIT_STYLE incarnation, by passing an extra argument to the widget's20EDIT_STYLE incarnation, by passing an extra argument to the widget's
21constructor (or to custom_widget(), if you're using it).21constructor (or to CustomWidgetFactory(), if you're using it).
2222
23Our policy is not to ask people to upload images when creating a record,23Our policy is not to ask people to upload images when creating a record,
24but instead to expose this as an edit form after the object is created.24but instead to expose this as an edit form after the object is created.
2525
=== 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-08-09 15:10:30 +0000
@@ -62,6 +62,7 @@
62from zope.event import notify62from zope.event import notify
63from zope.formlib import form63from zope.formlib import form
64from zope.formlib.form import Fields64from zope.formlib.form import Fields
65from zope.formlib.widget import CustomWidgetFactory
65from zope.formlib.widgets import (66from zope.formlib.widgets import (
66 TextAreaWidget,67 TextAreaWidget,
67 TextWidget,68 TextWidget,
@@ -82,7 +83,6 @@
82from lp.app.browser.launchpad import AppFrontPageSearchView83from lp.app.browser.launchpad import AppFrontPageSearchView
83from lp.app.browser.launchpadform import (84from lp.app.browser.launchpadform import (
84 action,85 action,
85 custom_widget,
86 LaunchpadEditFormView,86 LaunchpadEditFormView,
87 LaunchpadFormView,87 LaunchpadFormView,
88 safe_action,88 safe_action,
@@ -206,8 +206,8 @@
206 page_title = 'Register a blueprint in Launchpad'206 page_title = 'Register a blueprint in Launchpad'
207 label = "Register a new blueprint"207 label = "Register a new blueprint"
208208
209 custom_widget('specurl', TextWidget, displayWidth=60)209 custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
210 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)210 custom_widget_information_type = LaunchpadRadioWidgetWithDescription
211211
212 def append_info_type(self, fields):212 def append_info_type(self, fields):
213 """Append an InformationType field for creating a Specification.213 """Append an InformationType field for creating a Specification.
@@ -790,9 +790,9 @@
790 schema = SpecificationEditSchema790 schema = SpecificationEditSchema
791 field_names = ['name', 'title', 'specurl', 'summary', 'whiteboard']791 field_names = ['name', 'title', 'specurl', 'summary', 'whiteboard']
792 label = 'Edit specification'792 label = 'Edit specification'
793 custom_widget('summary', TextAreaWidget, height=5)793 custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
794 custom_widget('whiteboard', TextAreaWidget, height=10)794 custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=10)
795 custom_widget('specurl', TextWidget, displayWidth=60)795 custom_widget_specurl = CustomWidgetFactory(TextWidget, displayWidth=60)
796796
797 @property797 @property
798 def adapters(self):798 def adapters(self):
@@ -815,13 +815,14 @@
815class SpecificationEditWhiteboardView(SpecificationEditView):815class SpecificationEditWhiteboardView(SpecificationEditView):
816 label = 'Edit specification status whiteboard'816 label = 'Edit specification status whiteboard'
817 field_names = ['whiteboard']817 field_names = ['whiteboard']
818 custom_widget('whiteboard', TextAreaWidget, height=15)818 custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=15)
819819
820820
821class SpecificationEditWorkItemsView(SpecificationEditView):821class SpecificationEditWorkItemsView(SpecificationEditView):
822 label = 'Edit specification work items'822 label = 'Edit specification work items'
823 field_names = ['workitems_text']823 field_names = ['workitems_text']
824 custom_widget('workitems_text', TextAreaWidget, height=15)824 custom_widget_workitems_text = CustomWidgetFactory(
825 TextAreaWidget, height=15)
825826
826 @action(_('Change'), name='change')827 @action(_('Change'), name='change')
827 def change_action(self, action, data):828 def change_action(self, action, data):
@@ -863,7 +864,7 @@
863864
864 field_names = ['information_type']865 field_names = ['information_type']
865866
866 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)867 custom_widget_information_type = LaunchpadRadioWidgetWithDescription
867868
868 @property869 @property
869 def schema(self):870 def schema(self):
@@ -913,7 +914,7 @@
913 schema = ISpecification914 schema = ISpecification
914 label = 'Target to a distribution series'915 label = 'Target to a distribution series'
915 field_names = ['distroseries', 'whiteboard']916 field_names = ['distroseries', 'whiteboard']
916 custom_widget('whiteboard', TextAreaWidget, height=5)917 custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
917918
918 @property919 @property
919 def initial_values(self):920 def initial_values(self):
920921
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Sprint views."""4"""Sprint views."""
@@ -30,13 +30,13 @@
30from lazr.restful.utils import smartquote30from lazr.restful.utils import smartquote
31import pytz31import pytz
32from zope.component import getUtility32from zope.component import getUtility
33from zope.formlib.widget import CustomWidgetFactory
33from zope.formlib.widgets import TextAreaWidget34from zope.formlib.widgets import TextAreaWidget
34from zope.interface import implementer35from zope.interface import implementer
3536
36from lp import _37from lp import _
37from lp.app.browser.launchpadform import (38from lp.app.browser.launchpadform import (
38 action,39 action,
39 custom_widget,
40 LaunchpadEditFormView,40 LaunchpadEditFormView,
41 LaunchpadFormView,41 LaunchpadFormView,
42 )42 )
@@ -261,10 +261,12 @@
261 'time_zone', 'time_starts', 'time_ends', 'is_physical',261 'time_zone', 'time_starts', 'time_ends', 'is_physical',
262 'address',262 'address',
263 ]263 ]
264 custom_widget('summary', TextAreaWidget, height=5)264 custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
265 custom_widget('time_starts', DateTimeWidget, display_zone=False)265 custom_widget_time_starts = CustomWidgetFactory(
266 custom_widget('time_ends', DateTimeWidget, display_zone=False)266 DateTimeWidget, display_zone=False)
267 custom_widget('address', TextAreaWidget, height=3)267 custom_widget_time_ends = CustomWidgetFactory(
268 DateTimeWidget, display_zone=False)
269 custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
268270
269 sprint = None271 sprint = None
270272
@@ -331,10 +333,12 @@
331 'time_zone', 'time_starts', 'time_ends', 'is_physical',333 'time_zone', 'time_starts', 'time_ends', 'is_physical',
332 'address',334 'address',
333 ]335 ]
334 custom_widget('summary', TextAreaWidget, height=5)336 custom_widget_summary = CustomWidgetFactory(TextAreaWidget, height=5)
335 custom_widget('time_starts', DateTimeWidget, display_zone=False)337 custom_widget_time_starts = CustomWidgetFactory(
336 custom_widget('time_ends', DateTimeWidget, display_zone=False)338 DateTimeWidget, display_zone=False)
337 custom_widget('address', TextAreaWidget, height=3)339 custom_widget_time_ends = CustomWidgetFactory(
340 DateTimeWidget, display_zone=False)
341 custom_widget_address = CustomWidgetFactory(TextAreaWidget, height=3)
338342
339 def setUpWidgets(self):343 def setUpWidgets(self):
340 LaunchpadEditFormView.setUpWidgets(self)344 LaunchpadEditFormView.setUpWidgets(self)
341345
=== 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-08-09 15:10:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Views for SprintAttendance."""4"""Views for SprintAttendance."""
@@ -12,11 +12,11 @@
12from datetime import timedelta12from datetime import timedelta
1313
14import pytz14import pytz
15from zope.formlib.widget import CustomWidgetFactory
1516
16from lp import _17from lp import _
17from lp.app.browser.launchpadform import (18from lp.app.browser.launchpadform import (
18 action,19 action,
19 custom_widget,
20 LaunchpadFormView,20 LaunchpadFormView,
21 )21 )
22from lp.app.widgets.date import DateTimeWidget22from lp.app.widgets.date import DateTimeWidget
@@ -28,10 +28,10 @@
28class BaseSprintAttendanceAddView(LaunchpadFormView):28class BaseSprintAttendanceAddView(LaunchpadFormView):
2929
30 schema = ISprintAttendance30 schema = ISprintAttendance
31 custom_widget('time_starts', DateTimeWidget)31 custom_widget_time_starts = DateTimeWidget
32 custom_widget('time_ends', DateTimeWidget)32 custom_widget_time_ends = DateTimeWidget
33 custom_widget(33 custom_widget_is_physical = CustomWidgetFactory(
34 'is_physical', LaunchpadBooleanRadioWidget, orientation='vertical',34 LaunchpadBooleanRadioWidget, orientation='vertical',
35 true_label="Physically", false_label="Remotely", hint=None)35 true_label="Physically", false_label="Remotely", hint=None)
3636
37 @property37 @property