Merge lp:~thumper/launchpad/refactor-lazrjs-text-widgets into lp:launchpad
- refactor-lazrjs-text-widgets
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Tim Penhey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12285 |
Proposed branch: | lp:~thumper/launchpad/refactor-lazrjs-text-widgets |
Merge into: | lp:launchpad |
Diff against target: |
1643 lines (+602/-536) 22 files modified
lib/lp/app/browser/lazrjs.py (+195/-274) lib/lp/app/browser/tests/test_inlineeditpickerwidget.py (+8/-8) lib/lp/app/browser/webservice.py (+2/-10) lib/lp/app/doc/lazr-js-widgets.txt (+237/-96) lib/lp/app/javascript/picker.js (+1/-0) lib/lp/app/templates/inline-picker.pt (+1/-1) lib/lp/app/templates/text-area-editor.pt (+40/-0) lib/lp/app/templates/text-line-editor.pt (+25/-0) lib/lp/bugs/browser/bugtask.py (+12/-17) lib/lp/bugs/templates/bugtask-index.pt (+1/-3) lib/lp/bugs/windmill/tests/test_mark_duplicate.py (+1/-1) lib/lp/code/browser/branch.py (+1/-1) lib/lp/code/browser/branchmergeproposal.py (+24/-29) lib/lp/code/browser/sourcepackagerecipe.py (+5/-10) lib/lp/code/templates/branchmergeproposal-index.pt (+14/-36) lib/lp/registry/browser/product.py (+11/-15) lib/lp/registry/windmill/tests/test_product.py (+2/-2) lib/lp/soyuz/browser/archive.py (+16/-26) lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt (+3/-2) lib/lp/soyuz/templates/archive-index.pt (+1/-3) lib/lp/soyuz/windmill/tests/test_ppainlineedit.py (+1/-1) lib/lp/translations/browser/hastranslationimports.py (+1/-1) |
To merge this branch: | bzr merge lp:~thumper/launchpad/refactor-lazrjs-text-widgets |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | Approve | ||
Review via email: mp+47634@code.launchpad.net |
Commit message
[r=flacoste]
Description of the change
This branch started out as a quick refactoring of the TextLineEditorW
It rapidly grew to tearing the widgets to shreds and remaking them.
The code that is now needed in the view classes to create the widgets is greatly reduced, and rendering the widgets is trivial regardless of their type.
The documentation was updated significantly, and is now actual documentation on use of the widgets.
Tim Penhey (thumper) wrote : | # |
On Fri, 28 Jan 2011 05:43:28 Francis J. Lacoste wrote:
> > === modified file 'lib/canonical/
> > +class WidgetBase:
> > + """Useful methods for all widgets."""
> >
> > + def __init__(self, context, exported_field, content_box_id):
> > + self.context = context
> > + self.exported_field = exported_field
> > +
> > + self.request = get_current_
>
> That makes us depend on global state. To unit test widget python code, it
> will require setting the interaction.
The widgets have always depended on global state as they call the
canonical_url method. This just makes that more dependency explicit.
> Maybe it's not that much of a problem...
>
> > + self.attribute_name = exported_
> > +
> > + if content_box_id is None:
> > + content_box_id = "edit-%s" % self.attribute_name
>
> We need to cater for the use-case when we'll have multiple edit widget on
> the page, so I'd suggest putting in a counter there. (That's why we were
> using _generate_id() before)
As we discussed, the _generate_id method was never called from any call-site
as they almost all used "edit-foo" where foo was the attribute being edited.
I updated the default to do this, and removed the extra parameter from the
call sites to make them simpler.
If for some reason there are multiple widgets on a page for editing a single
attribute name (perhaps like bug task assignees), then the call site will need
to specify the id.
> Why do we need to care about the mutator method name? It should be
> transparent: you simply PATCH the field to the new value and lazr.restful
> will call the mutator appropriately automatically. The 'as' stuff is
> required though.
Comment added.
> > + @property
> > + def resource_uri(self):
> > + return canonical_
> > +
>
> Why force_local_path? (Subtle hint to add a comment there.)
Docstring added.
> > +class TextWidgetBase(
> > +
> > + def __init__(self, context, exported_field, content_box_id,
> > + accept_empty, title, edit_view, edit_url):
> > + super(TextWidge
> > + context, exported_field, content_box_id)
> > + if edit_url is None:
> > + edit_url = canonical_
>
> If edit_url can be None, why not make it an optional argument? I see that
> you made it so in the descendant class.
No I don't want it optional here. TextWidgetBase is an abstract base class
for the text editor widgets. Even though they have the paramter as optional,
it is explicitly passed through to the base class constructor.
> > + resource_uri = LP.client.
>
> Aren't you missing a var here?
Nope. It is altering the function parameter.
> WHy do we have 'Set description' links in addition to the widget? If it's
> for graceful degradation, could we make that part of the widget API? Like
> it is for the inline textline widget?
This is due to the behaviour on the merge proposal page for empty text areas
not showing, and a link appearing. As we discussed it may be...
Francis J. Lacoste (flacoste) wrote : | # |
Really nice!
Thanks for the extensive rewrite of the documentation!
Preview Diff
1 | === renamed file 'lib/canonical/widgets/lazrjs.py' => 'lib/lp/app/browser/lazrjs.py' | |||
2 | --- lib/canonical/widgets/lazrjs.py 2011-01-21 04:32:26 +0000 | |||
3 | +++ lib/lp/app/browser/lazrjs.py 2011-01-30 20:24:55 +0000 | |||
4 | @@ -1,19 +1,18 @@ | |||
6 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Wrappers for lazr-js widgets.""" | 4 | """Wrappers for lazr-js widgets.""" |
10 | 5 | 5 | ||
11 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
12 | 7 | __all__ = [ | 7 | __all__ = [ |
13 | 8 | 'InlineEditPickerWidget', | ||
14 | 9 | 'standard_text_html_representation', | ||
15 | 8 | 'TextAreaEditorWidget', | 10 | 'TextAreaEditorWidget', |
17 | 9 | 'InlineEditPickerWidget', | 11 | 'TextLineEditorWidget', |
18 | 10 | 'vocabulary_to_choice_edit_items', | 12 | 'vocabulary_to_choice_edit_items', |
19 | 11 | 'TextLineEditorWidget', | ||
20 | 12 | ] | 13 | ] |
21 | 13 | 14 | ||
22 | 14 | import cgi | ||
23 | 15 | import simplejson | 15 | import simplejson |
24 | 16 | from textwrap import dedent | ||
25 | 17 | 16 | ||
26 | 18 | from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile | 17 | from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile |
27 | 19 | from zope.component import getUtility | 18 | from zope.component import getUtility |
28 | @@ -21,255 +20,209 @@ | |||
29 | 21 | from zope.schema.interfaces import IVocabulary | 20 | from zope.schema.interfaces import IVocabulary |
30 | 22 | from zope.schema.vocabulary import getVocabularyRegistry | 21 | from zope.schema.vocabulary import getVocabularyRegistry |
31 | 23 | 22 | ||
33 | 24 | from lazr.restful.interfaces import IWebServiceClientRequest | 23 | from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED |
34 | 24 | from canonical.lazr.utils import get_current_browser_request | ||
35 | 25 | from canonical.lazr.utils import safe_hasattr | 25 | from canonical.lazr.utils import safe_hasattr |
36 | 26 | 26 | ||
37 | 27 | from canonical.launchpad.webapp.interfaces import ILaunchBag | 27 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
38 | 28 | from canonical.launchpad.webapp.publisher import canonical_url | 28 | from canonical.launchpad.webapp.publisher import canonical_url |
39 | 29 | from canonical.launchpad.webapp.vocabulary import IHugeVocabulary | 29 | from canonical.launchpad.webapp.vocabulary import IHugeVocabulary |
40 | 30 | from lp.app.browser.stringformatter import FormattersAPI | ||
41 | 30 | from lp.services.propertycache import cachedproperty | 31 | from lp.services.propertycache import cachedproperty |
42 | 31 | 32 | ||
43 | 32 | 33 | ||
45 | 33 | class TextLineEditorWidget: | 34 | class WidgetBase: |
46 | 35 | """Useful methods for all widgets.""" | ||
47 | 36 | |||
48 | 37 | def __init__(self, context, exported_field, content_box_id): | ||
49 | 38 | self.context = context | ||
50 | 39 | self.exported_field = exported_field | ||
51 | 40 | |||
52 | 41 | self.request = get_current_browser_request() | ||
53 | 42 | self.attribute_name = exported_field.__name__ | ||
54 | 43 | self.optional_field = not exported_field.required | ||
55 | 44 | |||
56 | 45 | if content_box_id is None: | ||
57 | 46 | content_box_id = "edit-%s" % self.attribute_name | ||
58 | 47 | self.content_box_id = content_box_id | ||
59 | 48 | |||
60 | 49 | # The mutator method name is used to determine whether or not the | ||
61 | 50 | # current user has permission to alter the attribute if the attribute | ||
62 | 51 | # is using a mutator function. | ||
63 | 52 | self.mutator_method_name = None | ||
64 | 53 | ws_stack = exported_field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) | ||
65 | 54 | if ws_stack is None: | ||
66 | 55 | # The field may be a copy, or similarly named to one we care | ||
67 | 56 | # about. | ||
68 | 57 | self.api_attribute = self.attribute_name | ||
69 | 58 | else: | ||
70 | 59 | self.api_attribute = ws_stack['as'] | ||
71 | 60 | mutator_info = ws_stack.get('mutator_annotations') | ||
72 | 61 | if mutator_info is not None: | ||
73 | 62 | mutator_method, mutator_extra = mutator_info | ||
74 | 63 | self.mutator_method_name = mutator_method.__name__ | ||
75 | 64 | |||
76 | 65 | @property | ||
77 | 66 | def resource_uri(self): | ||
78 | 67 | """A local path to the context object. | ||
79 | 68 | |||
80 | 69 | The javascript uses the normalize_uri method that adds the appropriate | ||
81 | 70 | prefix to the uri. Doing it this way avoids needing to adapt the | ||
82 | 71 | current request into a webservice request in order to get an api url. | ||
83 | 72 | """ | ||
84 | 73 | return canonical_url(self.context, force_local_path=True) | ||
85 | 74 | |||
86 | 75 | @property | ||
87 | 76 | def json_resource_uri(self): | ||
88 | 77 | return simplejson.dumps(self.resource_uri) | ||
89 | 78 | |||
90 | 79 | @property | ||
91 | 80 | def can_write(self): | ||
92 | 81 | """Can the current user write to the attribute.""" | ||
93 | 82 | if canWrite(self.context, self.attribute_name): | ||
94 | 83 | return True | ||
95 | 84 | elif self.mutator_method_name is not None: | ||
96 | 85 | # The user may not have write access on the attribute itself, but | ||
97 | 86 | # the REST API may have a mutator method configured, such as | ||
98 | 87 | # transitionToAssignee. | ||
99 | 88 | return canAccess(self.context, self.mutator_method_name) | ||
100 | 89 | else: | ||
101 | 90 | return False | ||
102 | 91 | |||
103 | 92 | |||
104 | 93 | class TextWidgetBase(WidgetBase): | ||
105 | 94 | """Abstract base for the single and multiline text editor widgets.""" | ||
106 | 95 | |||
107 | 96 | def __init__(self, context, exported_field, title, content_box_id, | ||
108 | 97 | edit_view, edit_url): | ||
109 | 98 | super(TextWidgetBase, self).__init__( | ||
110 | 99 | context, exported_field, content_box_id) | ||
111 | 100 | if edit_url is None: | ||
112 | 101 | edit_url = canonical_url(self.context, view_name=edit_view) | ||
113 | 102 | self.edit_url = edit_url | ||
114 | 103 | self.accept_empty = simplejson.dumps(self.optional_field) | ||
115 | 104 | self.title = title | ||
116 | 105 | self.json_attribute = simplejson.dumps(self.api_attribute) | ||
117 | 106 | self.widget_css_selector = simplejson.dumps('#' + self.content_box_id) | ||
118 | 107 | |||
119 | 108 | @property | ||
120 | 109 | def json_attribute_uri(self): | ||
121 | 110 | return simplejson.dumps(self.resource_uri + '/' + self.api_attribute) | ||
122 | 111 | |||
123 | 112 | |||
124 | 113 | class TextLineEditorWidget(TextWidgetBase): | ||
125 | 34 | """Wrapper for the lazr-js inlineedit/editor.js widget.""" | 114 | """Wrapper for the lazr-js inlineedit/editor.js widget.""" |
126 | 35 | 115 | ||
176 | 36 | # Class variable used to generate a unique per-page id for the widget | 116 | __call__ = ViewPageTemplateFile('../templates/text-line-editor.pt') |
177 | 37 | # in case it's not provided. | 117 | |
178 | 38 | last_id = 0 | 118 | def __init__(self, context, exported_field, title, tag, |
179 | 39 | 119 | content_box_id=None, edit_view="+edit", edit_url=None, | |
131 | 40 | # The HTML template used to render the widget. | ||
132 | 41 | # Replacements: | ||
133 | 42 | # activation_script: the JS script to active the widget | ||
134 | 43 | # attribute: the name of the being edited | ||
135 | 44 | # context_url: the url to the current context | ||
136 | 45 | # edit_url: the URL used to edit the value when JS is turned off | ||
137 | 46 | # id: the widget unique id | ||
138 | 47 | # title: the widget title | ||
139 | 48 | # trigger: the trigger (button) HTML code | ||
140 | 49 | # value: the current field value | ||
141 | 50 | WIDGET_TEMPLATE = dedent(u"""\ | ||
142 | 51 | <%(tag)s id="%(id)s"><span | ||
143 | 52 | class="yui3-editable_text-text">%(value)s</span> | ||
144 | 53 | %(trigger)s | ||
145 | 54 | </%(tag)s> | ||
146 | 55 | %(activation_script)s | ||
147 | 56 | """) | ||
148 | 57 | |||
149 | 58 | # Template for the trigger button. | ||
150 | 59 | TRIGGER_TEMPLATE = dedent(u"""\ | ||
151 | 60 | <a href="%(edit_url)s" class="yui3-editable_text-trigger sprite edit" | ||
152 | 61 | ></a> | ||
153 | 62 | """) | ||
154 | 63 | |||
155 | 64 | # Template for the activation script. | ||
156 | 65 | ACTIVATION_TEMPLATE = dedent(u"""\ | ||
157 | 66 | <script> | ||
158 | 67 | LPS.use('lazr.editor', 'lp.client.plugins', function (Y) { | ||
159 | 68 | var widget = new Y.EditableText({ | ||
160 | 69 | contentBox: '#%(id)s', | ||
161 | 70 | accept_empty: %(accept_empty)s, | ||
162 | 71 | width: '%(width)s', | ||
163 | 72 | initial_value_override: %(initial_value_override)s | ||
164 | 73 | }); | ||
165 | 74 | widget.editor.plug({ | ||
166 | 75 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { | ||
167 | 76 | patch: '%(public_attribute)s', | ||
168 | 77 | resource: '%(context_url)s'}}); | ||
169 | 78 | widget.render(); | ||
170 | 79 | }); | ||
171 | 80 | </script> | ||
172 | 81 | """) | ||
173 | 82 | |||
174 | 83 | def __init__(self, context, attribute, edit_url, id=None, title="Edit", | ||
175 | 84 | tag='h1', public_attribute=None, accept_empty=False, | ||
180 | 85 | default_text=None, initial_value_override=None, width=None): | 120 | default_text=None, initial_value_override=None, width=None): |
181 | 86 | """Create a widget wrapper. | 121 | """Create a widget wrapper. |
182 | 87 | 122 | ||
183 | 88 | :param context: The object that is being edited. | 123 | :param context: The object that is being edited. |
190 | 89 | :param attribute: The name of the attribute being edited. | 124 | :param exported_field: The attribute being edited. This should be |
191 | 90 | :param edit_url: The URL to use for editing when the user isn't logged | 125 | a field from an interface of the form ISomeInterface['fieldname'] |
192 | 91 | in and when JS is off. | 126 | :param title: The string to use as the link title. |
187 | 92 | :param id: The HTML id to use for this widget. Automatically | ||
188 | 93 | generated if this is not provided. | ||
189 | 94 | :param title: The string to use as the link title. Defaults to 'Edit'. | ||
193 | 95 | :param tag: The HTML tag to use. | 127 | :param tag: The HTML tag to use. |
197 | 96 | :param public_attribute: If given, the name of the attribute in the | 128 | :param content_box_id: The HTML id to use for this widget. |
198 | 97 | public webservice API. | 129 | Defaults to edit-<attribute name>. |
199 | 98 | :param accept_empty: Whether the field accepts empty input or not. | 130 | :param edit_view: The view name to use to generate the edit_url if |
200 | 131 | one is not specified. | ||
201 | 132 | :param edit_url: The URL to use for editing when the user isn't logged | ||
202 | 133 | in and when JS is off. Defaults to the edit_view on the context. | ||
203 | 99 | :param default_text: Text to show in the unedited field, if the | 134 | :param default_text: Text to show in the unedited field, if the |
204 | 100 | parameter value is missing or None. | 135 | parameter value is missing or None. |
205 | 101 | :param initial_value_override: Use this text for the initial edited | 136 | :param initial_value_override: Use this text for the initial edited |
206 | 102 | field value instead of the attribute's current value. | 137 | field value instead of the attribute's current value. |
207 | 103 | :param width: Initial widget width. | 138 | :param width: Initial widget width. |
208 | 104 | """ | 139 | """ |
212 | 105 | self.context = context | 140 | super(TextLineEditorWidget, self).__init__( |
213 | 106 | self.attribute = attribute | 141 | context, exported_field, title, content_box_id, |
214 | 107 | self.edit_url = edit_url | 142 | edit_view, edit_url) |
215 | 108 | self.tag = tag | 143 | self.tag = tag |
216 | 109 | if accept_empty: | ||
217 | 110 | self.accept_empty = 'true' | ||
218 | 111 | else: | ||
219 | 112 | self.accept_empty = 'false' | ||
220 | 113 | if public_attribute is None: | ||
221 | 114 | self.public_attribute = attribute | ||
222 | 115 | else: | ||
223 | 116 | self.public_attribute = public_attribute | ||
224 | 117 | if id is None: | ||
225 | 118 | self.id = self._generate_id() | ||
226 | 119 | else: | ||
227 | 120 | self.id = id | ||
228 | 121 | self.title = title | ||
229 | 122 | self.default_text = default_text | 144 | self.default_text = default_text |
272 | 123 | self.initial_value_override = initial_value_override | 145 | self.initial_value_override = simplejson.dumps(initial_value_override) |
273 | 124 | self.width = width | 146 | self.width = simplejson.dumps(width) |
274 | 125 | 147 | ||
275 | 126 | @classmethod | 148 | @property |
276 | 127 | def _generate_id(cls): | 149 | def open_tag(self): |
277 | 128 | """Return a presumably unique id for this widget.""" | 150 | return '<%s id="%s">' % (self.tag, self.content_box_id) |
278 | 129 | cls.last_id += 1 | 151 | |
279 | 130 | return 'inline-textline-editor-id%d' % cls.last_id | 152 | @property |
280 | 131 | 153 | def close_tag(self): | |
281 | 132 | def __call__(self): | 154 | return '</%s>' % self.tag |
282 | 133 | """Return the HTML to include to render the widget.""" | 155 | |
283 | 134 | # We can't use the value None because of the cgi.escape() and because | 156 | @property |
284 | 135 | # that wouldn't look very good in the ui! | 157 | def value(self): |
285 | 136 | value = getattr(self.context, self.attribute, self.default_text) | 158 | text = getattr(self.context, self.attribute_name, self.default_text) |
286 | 137 | if value is None: | 159 | if text is None: |
287 | 138 | value = self.default_text | 160 | text = self.default_text |
288 | 139 | params = { | 161 | return text |
289 | 140 | 'activation_script': '', | 162 | |
290 | 141 | 'trigger': '', | 163 | |
291 | 142 | 'edit_url': self.edit_url, | 164 | class TextAreaEditorWidget(TextWidgetBase): |
250 | 143 | 'id': self.id, | ||
251 | 144 | 'title': self.title, | ||
252 | 145 | 'value': cgi.escape(value), | ||
253 | 146 | 'context_url': canonical_url( | ||
254 | 147 | self.context, path_only_if_possible=True), | ||
255 | 148 | 'attribute': self.attribute, | ||
256 | 149 | 'tag': self.tag, | ||
257 | 150 | 'public_attribute': self.public_attribute, | ||
258 | 151 | 'accept_empty': self.accept_empty, | ||
259 | 152 | 'initial_value_override': simplejson.dumps( | ||
260 | 153 | self.initial_value_override), | ||
261 | 154 | 'width': self.width, | ||
262 | 155 | } | ||
263 | 156 | # Only display the trigger link and the activation script if | ||
264 | 157 | # the user can write the attribute. | ||
265 | 158 | if canWrite(self.context, self.attribute): | ||
266 | 159 | params['trigger'] = self.TRIGGER_TEMPLATE % params | ||
267 | 160 | params['activation_script'] = self.ACTIVATION_TEMPLATE % params | ||
268 | 161 | return self.WIDGET_TEMPLATE % params | ||
269 | 162 | |||
270 | 163 | |||
271 | 164 | class TextAreaEditorWidget(TextLineEditorWidget): | ||
292 | 165 | """Wrapper for the multine-line lazr-js inlineedit/editor.js widget.""" | 165 | """Wrapper for the multine-line lazr-js inlineedit/editor.js widget.""" |
293 | 166 | 166 | ||
382 | 167 | def __init__(self, *args, **kwds): | 167 | __call__ = ViewPageTemplateFile('../templates/text-area-editor.pt') |
383 | 168 | """Create the widget wrapper.""" | 168 | |
384 | 169 | if 'value' in kwds: | 169 | def __init__(self, context, exported_field, title, content_box_id=None, |
385 | 170 | self.value = kwds.get('value', '') | 170 | edit_view="+edit", edit_url=None, |
386 | 171 | kwds.pop('value') | 171 | hide_empty=True, linkify_text=True): |
387 | 172 | super(TextAreaEditorWidget, self).__init__(*args, **kwds) | 172 | """Create the widget wrapper. |
388 | 173 | 173 | ||
389 | 174 | # The HTML template used to render the widget. | 174 | :param context: The object that is being edited. |
390 | 175 | # Replacements: | 175 | :param exported_field: The attribute being edited. This should be |
391 | 176 | # activation_script: the JS script to active the widget | 176 | a field from an interface of the form ISomeInterface['fieldname'] |
392 | 177 | # attribute: the name of the being edited | 177 | :param title: The string to use as the link title. |
393 | 178 | # context_url: the url to the current context | 178 | :param content_box_id: The HTML id to use for this widget. |
394 | 179 | # edit_url: the URL used to edit the value when JS is turned off | 179 | Defaults to edit-<attribute name>. |
395 | 180 | # id: the widget unique id | 180 | :param edit_view: The view name to use to generate the edit_url if |
396 | 181 | # title: the widget title | 181 | one is not specified. |
397 | 182 | # trigger: the trigger (button) HTML code | 182 | :param edit_url: The URL to use for editing when the user isn't logged |
398 | 183 | # value: the current field value | 183 | in and when JS is off. Defaults to the edit_view on the context. |
399 | 184 | WIDGET_TEMPLATE = dedent(u"""\ | 184 | :param hide_empty: If the attribute has no value, or is empty, then |
400 | 185 | <div id="multi-text-editor"> | 185 | hide the editor by adding the "unseen" CSS class. |
401 | 186 | <div class="clearfix"> | 186 | :param linkify_text: If True the HTML version of the text will have |
402 | 187 | %(edit_controls)s | 187 | things that look like links made into anchors. |
403 | 188 | <h2>%(title)s</h2> | 188 | """ |
404 | 189 | </div> | 189 | super(TextAreaEditorWidget, self).__init__( |
405 | 190 | <div class="yui3-editable_text-text">%(value)s</div> | 190 | context, exported_field, title, content_box_id, |
406 | 191 | </div> | 191 | edit_view, edit_url) |
407 | 192 | %(activation_script)s | 192 | self.hide_empty = hide_empty |
408 | 193 | """) | 193 | self.linkify_text = linkify_text |
409 | 194 | 194 | ||
410 | 195 | CONTROLS_TEMPLATE = dedent(u"""\ | 195 | @property |
411 | 196 | <div class="edit-controls"> | 196 | def tag_class(self): |
412 | 197 | | 197 | """The CSS class for the widget.""" |
413 | 198 | %(trigger)s | 198 | classes = ['lazr-multiline-edit'] |
414 | 199 | </div> | 199 | if self.hide_empty and not self.value: |
415 | 200 | """) | 200 | classes.append('unseen') |
416 | 201 | 201 | return ' '.join(classes) | |
417 | 202 | ACTIVATION_TEMPLATE = dedent(u"""\ | 202 | |
418 | 203 | <script> | 203 | @cachedproperty |
419 | 204 | LPS.use('lazr.editor', 'lp.client.plugins', function (Y) { | 204 | def value(self): |
420 | 205 | var widget = new Y.EditableText({ | 205 | text = getattr(self.context, self.attribute_name, None) |
421 | 206 | contentBox: '#%(id)s', | 206 | return standard_text_html_representation(text, self.linkify_text) |
422 | 207 | accept_empty: %(accept_empty)s, | 207 | |
423 | 208 | multiline: true, | 208 | |
424 | 209 | buttons: 'top' | 209 | class InlineEditPickerWidget(WidgetBase): |
337 | 210 | }); | ||
338 | 211 | widget.editor.plug({ | ||
339 | 212 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { | ||
340 | 213 | patch: '%(attribute)s', | ||
341 | 214 | resource: '%(context_url)s/%(attribute)s', | ||
342 | 215 | patch_field: true, | ||
343 | 216 | accept: 'application/xhtml+xml' | ||
344 | 217 | }}); | ||
345 | 218 | if (!Y.UA.opera) { | ||
346 | 219 | widget.render(); | ||
347 | 220 | } | ||
348 | 221 | var lpns = Y.namespace('lp'); | ||
349 | 222 | if (!lpns.widgets) { | ||
350 | 223 | lpns.widgets = {}; | ||
351 | 224 | } | ||
352 | 225 | lpns.widgets['%(id)s'] = widget; | ||
353 | 226 | }); | ||
354 | 227 | </script> | ||
355 | 228 | """) | ||
356 | 229 | |||
357 | 230 | def __call__(self): | ||
358 | 231 | """Return the HTML to include to render the widget.""" | ||
359 | 232 | params = { | ||
360 | 233 | 'activation_script': '', | ||
361 | 234 | 'trigger': '', | ||
362 | 235 | 'edit_url': self.edit_url, | ||
363 | 236 | 'id': self.id, | ||
364 | 237 | 'title': self.title, | ||
365 | 238 | 'value': self.value, | ||
366 | 239 | 'context_url': canonical_url( | ||
367 | 240 | self.context, path_only_if_possible=True), | ||
368 | 241 | 'attribute': self.attribute, | ||
369 | 242 | 'accept_empty': self.accept_empty, | ||
370 | 243 | 'edit_controls': '', | ||
371 | 244 | } | ||
372 | 245 | # Only display the trigger link and the activation script if | ||
373 | 246 | # the user can write the attribute. | ||
374 | 247 | if canWrite(self.context, self.attribute): | ||
375 | 248 | params['trigger'] = self.TRIGGER_TEMPLATE % params | ||
376 | 249 | params['activation_script'] = self.ACTIVATION_TEMPLATE % params | ||
377 | 250 | params['edit_controls'] = self.CONTROLS_TEMPLATE % params | ||
378 | 251 | return self.WIDGET_TEMPLATE % params | ||
379 | 252 | |||
380 | 253 | |||
381 | 254 | class InlineEditPickerWidget: | ||
425 | 255 | """Wrapper for the lazr-js picker widget. | 210 | """Wrapper for the lazr-js picker widget. |
426 | 256 | 211 | ||
427 | 257 | This widget is not for editing form values like the | 212 | This widget is not for editing form values like the |
428 | 258 | VocabularyPickerWidget. | 213 | VocabularyPickerWidget. |
429 | 259 | """ | 214 | """ |
430 | 260 | 215 | ||
433 | 261 | last_id = 0 | 216 | __call__ = ViewPageTemplateFile('../templates/inline-picker.pt') |
432 | 262 | __call__ = ViewPageTemplateFile('templates/inline-picker.pt') | ||
434 | 263 | 217 | ||
436 | 264 | def __init__(self, context, request, interface_attribute, default_html, | 218 | def __init__(self, context, exported_field, default_html, |
437 | 265 | content_box_id=None, header='Select an item', | 219 | content_box_id=None, header='Select an item', |
438 | 266 | step_title='Search', remove_button_text='Remove', | 220 | step_title='Search', remove_button_text='Remove', |
439 | 267 | null_display_value='None'): | 221 | null_display_value='None'): |
440 | 268 | """Create a widget wrapper. | 222 | """Create a widget wrapper. |
441 | 269 | 223 | ||
442 | 270 | :param context: The object that is being edited. | 224 | :param context: The object that is being edited. |
445 | 271 | :param request: The request object. | 225 | :param exported_field: The attribute being edited. This should be |
444 | 272 | :param interface_attribute: The attribute being edited. This should be | ||
446 | 273 | a field from an interface of the form ISomeInterface['fieldname'] | 226 | a field from an interface of the form ISomeInterface['fieldname'] |
447 | 274 | :param default_html: Default display of attribute. | 227 | :param default_html: Default display of attribute. |
448 | 275 | :param content_box_id: The HTML id to use for this widget. Automatically | 228 | :param content_box_id: The HTML id to use for this widget. Automatically |
449 | @@ -279,17 +232,9 @@ | |||
450 | 279 | :param remove_button_text: Override default button text: "Remove" | 232 | :param remove_button_text: Override default button text: "Remove" |
451 | 280 | :param null_display_value: This will be shown for a missing value | 233 | :param null_display_value: This will be shown for a missing value |
452 | 281 | """ | 234 | """ |
455 | 282 | self.context = context | 235 | super(InlineEditPickerWidget, self).__init__( |
456 | 283 | self.request = request | 236 | context, exported_field, content_box_id) |
457 | 284 | self.default_html = default_html | 237 | self.default_html = default_html |
458 | 285 | self.interface_attribute = interface_attribute | ||
459 | 286 | self.attribute_name = interface_attribute.__name__ | ||
460 | 287 | |||
461 | 288 | if content_box_id is None: | ||
462 | 289 | self.content_box_id = self._generate_id() | ||
463 | 290 | else: | ||
464 | 291 | self.content_box_id = content_box_id | ||
465 | 292 | |||
466 | 293 | self.header = header | 238 | self.header = header |
467 | 294 | self.step_title = step_title | 239 | self.step_title = step_title |
468 | 295 | self.remove_button_text = remove_button_text | 240 | self.remove_button_text = remove_button_text |
469 | @@ -297,26 +242,29 @@ | |||
470 | 297 | 242 | ||
471 | 298 | # JSON encoded attributes. | 243 | # JSON encoded attributes. |
472 | 299 | self.json_content_box_id = simplejson.dumps(self.content_box_id) | 244 | self.json_content_box_id = simplejson.dumps(self.content_box_id) |
474 | 300 | self.json_attribute = simplejson.dumps(self.attribute_name + '_link') | 245 | self.json_attribute = simplejson.dumps(self.api_attribute + '_link') |
475 | 301 | self.json_vocabulary_name = simplejson.dumps( | 246 | self.json_vocabulary_name = simplejson.dumps( |
478 | 302 | self.interface_attribute.vocabularyName) | 247 | self.exported_field.vocabularyName) |
477 | 303 | self.show_remove_button = not self.interface_attribute.required | ||
479 | 304 | 248 | ||
480 | 305 | @property | 249 | @property |
481 | 306 | def config(self): | 250 | def config(self): |
489 | 307 | return simplejson.dumps( | 251 | return dict( |
490 | 308 | dict(header=self.header, step_title=self.step_title, | 252 | header=self.header, step_title=self.step_title, |
491 | 309 | remove_button_text=self.remove_button_text, | 253 | remove_button_text=self.remove_button_text, |
492 | 310 | null_display_value=self.null_display_value, | 254 | null_display_value=self.null_display_value, |
493 | 311 | show_remove_button=self.show_remove_button, | 255 | show_remove_button=self.optional_field, |
494 | 312 | show_assign_me_button=self.show_assign_me_button, | 256 | show_assign_me_button=self.show_assign_me_button, |
495 | 313 | show_search_box=self.show_search_box)) | 257 | show_search_box=self.show_search_box) |
496 | 258 | |||
497 | 259 | @property | ||
498 | 260 | def json_config(self): | ||
499 | 261 | return simplejson.dumps(self.config) | ||
500 | 314 | 262 | ||
501 | 315 | @cachedproperty | 263 | @cachedproperty |
502 | 316 | def vocabulary(self): | 264 | def vocabulary(self): |
503 | 317 | registry = getVocabularyRegistry() | 265 | registry = getVocabularyRegistry() |
504 | 318 | return registry.get( | 266 | return registry.get( |
506 | 319 | IVocabulary, self.interface_attribute.vocabularyName) | 267 | IVocabulary, self.exported_field.vocabularyName) |
507 | 320 | 268 | ||
508 | 321 | @property | 269 | @property |
509 | 322 | def show_search_box(self): | 270 | def show_search_box(self): |
510 | @@ -329,43 +277,6 @@ | |||
511 | 329 | user = getUtility(ILaunchBag).user | 277 | user = getUtility(ILaunchBag).user |
512 | 330 | return user and user in vocabulary | 278 | return user and user in vocabulary |
513 | 331 | 279 | ||
514 | 332 | @classmethod | ||
515 | 333 | def _generate_id(cls): | ||
516 | 334 | """Return a presumably unique id for this widget.""" | ||
517 | 335 | cls.last_id += 1 | ||
518 | 336 | return 'inline-picker-activator-id-%d' % cls.last_id | ||
519 | 337 | |||
520 | 338 | @property | ||
521 | 339 | def json_resource_uri(self): | ||
522 | 340 | return simplejson.dumps( | ||
523 | 341 | canonical_url( | ||
524 | 342 | self.context, request=IWebServiceClientRequest(self.request), | ||
525 | 343 | path_only_if_possible=True)) | ||
526 | 344 | |||
527 | 345 | @property | ||
528 | 346 | def can_write(self): | ||
529 | 347 | if canWrite(self.context, self.attribute_name): | ||
530 | 348 | return True | ||
531 | 349 | else: | ||
532 | 350 | # The user may not have write access on the attribute itself, but | ||
533 | 351 | # the REST API may have a mutator method configured, such as | ||
534 | 352 | # transitionToAssignee. | ||
535 | 353 | # | ||
536 | 354 | # We look at the top of the annotation stack, since Ajax | ||
537 | 355 | # requests always go to the most recent version of the web | ||
538 | 356 | # service. | ||
539 | 357 | try: | ||
540 | 358 | exported_tag_stack = self.interface_attribute.getTaggedValue( | ||
541 | 359 | 'lazr.restful.exported') | ||
542 | 360 | except KeyError: | ||
543 | 361 | return False | ||
544 | 362 | mutator_info = exported_tag_stack.get('mutator_annotations') | ||
545 | 363 | if mutator_info is not None: | ||
546 | 364 | mutator_method, mutator_extra = mutator_info | ||
547 | 365 | return canAccess(self.context, mutator_method.__name__) | ||
548 | 366 | else: | ||
549 | 367 | return False | ||
550 | 368 | |||
551 | 369 | 280 | ||
552 | 370 | def vocabulary_to_choice_edit_items( | 281 | def vocabulary_to_choice_edit_items( |
553 | 371 | vocab, css_class_prefix=None, disabled_items=None, as_json=False, | 282 | vocab, css_class_prefix=None, disabled_items=None, as_json=False, |
554 | @@ -412,3 +323,13 @@ | |||
555 | 412 | else: | 323 | else: |
556 | 413 | return items | 324 | return items |
557 | 414 | 325 | ||
558 | 326 | |||
559 | 327 | def standard_text_html_representation(value, linkify_text=True): | ||
560 | 328 | """Render a string for html display. | ||
561 | 329 | |||
562 | 330 | For this we obfuscate email and render as html. | ||
563 | 331 | """ | ||
564 | 332 | if value is None: | ||
565 | 333 | return '' | ||
566 | 334 | nomail = FormattersAPI(value).obfuscate_email() | ||
567 | 335 | return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text) | ||
568 | 415 | 336 | ||
569 | === renamed file 'lib/canonical/widgets/tests/test_inlineeditpickerwidget.py' => 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py' | |||
570 | --- lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 2011-01-20 21:16:36 +0000 | |||
571 | +++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2011-01-30 20:24:55 +0000 | |||
572 | @@ -9,7 +9,7 @@ | |||
573 | 9 | from zope.schema import Choice | 9 | from zope.schema import Choice |
574 | 10 | 10 | ||
575 | 11 | from canonical.testing.layers import DatabaseFunctionalLayer | 11 | from canonical.testing.layers import DatabaseFunctionalLayer |
577 | 12 | from canonical.widgets.lazrjs import InlineEditPickerWidget | 12 | from lp.app.browser.lazrjs import InlineEditPickerWidget |
578 | 13 | from lp.testing import ( | 13 | from lp.testing import ( |
579 | 14 | login_person, | 14 | login_person, |
580 | 15 | TestCaseWithFactory, | 15 | TestCaseWithFactory, |
581 | @@ -23,35 +23,35 @@ | |||
582 | 23 | def getWidget(self, **kwargs): | 23 | def getWidget(self, **kwargs): |
583 | 24 | class ITest(Interface): | 24 | class ITest(Interface): |
584 | 25 | test_field = Choice(**kwargs) | 25 | test_field = Choice(**kwargs) |
586 | 26 | return InlineEditPickerWidget(None, None, ITest['test_field'], None) | 26 | return InlineEditPickerWidget(None, ITest['test_field'], None) |
587 | 27 | 27 | ||
588 | 28 | def test_huge_vocabulary_is_searchable(self): | 28 | def test_huge_vocabulary_is_searchable(self): |
589 | 29 | # Make sure that when given a field for a huge vocabulary, the picker | 29 | # Make sure that when given a field for a huge vocabulary, the picker |
590 | 30 | # is set to show the search box. | 30 | # is set to show the search box. |
591 | 31 | widget = self.getWidget(vocabulary='ValidPersonOrTeam') | 31 | widget = self.getWidget(vocabulary='ValidPersonOrTeam') |
593 | 32 | self.assertTrue(widget.show_search_box) | 32 | self.assertTrue(widget.config['show_search_box']) |
594 | 33 | 33 | ||
595 | 34 | def test_normal_vocabulary_is_not_searchable(self): | 34 | def test_normal_vocabulary_is_not_searchable(self): |
596 | 35 | # Make sure that when given a field for a normal vocabulary, the picker | 35 | # Make sure that when given a field for a normal vocabulary, the picker |
597 | 36 | # is set to show the search box. | 36 | # is set to show the search box. |
598 | 37 | widget = self.getWidget(vocabulary='UserTeamsParticipation') | 37 | widget = self.getWidget(vocabulary='UserTeamsParticipation') |
600 | 38 | self.assertFalse(widget.show_search_box) | 38 | self.assertFalse(widget.config['show_search_box']) |
601 | 39 | 39 | ||
602 | 40 | def test_required_fields_dont_have_a_remove_link(self): | 40 | def test_required_fields_dont_have_a_remove_link(self): |
603 | 41 | widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True) | 41 | widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True) |
605 | 42 | self.assertFalse(widget.show_remove_button) | 42 | self.assertFalse(widget.config['show_remove_button']) |
606 | 43 | 43 | ||
607 | 44 | def test_optional_fields_do_have_a_remove_link(self): | 44 | def test_optional_fields_do_have_a_remove_link(self): |
608 | 45 | widget = self.getWidget( | 45 | widget = self.getWidget( |
609 | 46 | vocabulary='ValidPersonOrTeam', required=False) | 46 | vocabulary='ValidPersonOrTeam', required=False) |
611 | 47 | self.assertTrue(widget.show_remove_button) | 47 | self.assertTrue(widget.config['show_remove_button']) |
612 | 48 | 48 | ||
613 | 49 | def test_assign_me_exists_if_user_in_vocabulary(self): | 49 | def test_assign_me_exists_if_user_in_vocabulary(self): |
614 | 50 | widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True) | 50 | widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True) |
615 | 51 | login_person(self.factory.makePerson()) | 51 | login_person(self.factory.makePerson()) |
617 | 52 | self.assertTrue(widget.show_assign_me_button) | 52 | self.assertTrue(widget.config['show_assign_me_button']) |
618 | 53 | 53 | ||
619 | 54 | def test_assign_me_not_shown_if_user_not_in_vocabulary(self): | 54 | def test_assign_me_not_shown_if_user_not_in_vocabulary(self): |
620 | 55 | widget = self.getWidget(vocabulary='TargetPPAs', required=True) | 55 | widget = self.getWidget(vocabulary='TargetPPAs', required=True) |
621 | 56 | login_person(self.factory.makePerson()) | 56 | login_person(self.factory.makePerson()) |
623 | 57 | self.assertFalse(widget.show_assign_me_button) | 57 | self.assertFalse(widget.config['show_assign_me_button']) |
624 | 58 | 58 | ||
625 | === modified file 'lib/lp/app/browser/webservice.py' | |||
626 | --- lib/lp/app/browser/webservice.py 2011-01-20 22:27:42 +0000 | |||
627 | +++ lib/lp/app/browser/webservice.py 2011-01-30 20:24:55 +0000 | |||
628 | @@ -18,7 +18,7 @@ | |||
629 | 18 | ) | 18 | ) |
630 | 19 | from zope.schema.interfaces import IText | 19 | from zope.schema.interfaces import IText |
631 | 20 | 20 | ||
633 | 21 | from lp.app.browser.stringformatter import FormattersAPI | 21 | from lp.app.browser.lazrjs import standard_text_html_representation |
634 | 22 | from lp.app.browser.tales import format_link | 22 | from lp.app.browser.tales import format_link |
635 | 23 | 23 | ||
636 | 24 | 24 | ||
637 | @@ -44,12 +44,4 @@ | |||
638 | 44 | @implementer(IFieldHTMLRenderer) | 44 | @implementer(IFieldHTMLRenderer) |
639 | 45 | def text_xhtml_representation(context, field, request): | 45 | def text_xhtml_representation(context, field, request): |
640 | 46 | """Render text as XHTML using the webservice.""" | 46 | """Render text as XHTML using the webservice.""" |
650 | 47 | formatter = FormattersAPI | 47 | return standard_text_html_representation |
642 | 48 | |||
643 | 49 | def renderer(value): | ||
644 | 50 | if value is None: | ||
645 | 51 | return '' | ||
646 | 52 | nomail = formatter(value).obfuscate_email() | ||
647 | 53 | return formatter(nomail).text_to_html() | ||
648 | 54 | |||
649 | 55 | return renderer | ||
651 | 56 | 48 | ||
652 | === renamed file 'lib/canonical/launchpad/doc/lazr-js-widgets.txt' => 'lib/lp/app/doc/lazr-js-widgets.txt' | |||
653 | --- lib/canonical/launchpad/doc/lazr-js-widgets.txt 2011-01-21 04:36:36 +0000 | |||
654 | +++ lib/lp/app/doc/lazr-js-widgets.txt 2011-01-30 20:24:55 +0000 | |||
655 | @@ -1,31 +1,40 @@ | |||
656 | 1 | LAZR JS Wrappers | 1 | LAZR JS Wrappers |
657 | 2 | ================ | 2 | ================ |
658 | 3 | 3 | ||
661 | 4 | The canonical.widgets.lazrjs module contains a bunch of wrapper | 4 | The lp.app.browser.lazrjs module contains several classes that simplify the |
662 | 5 | for widgets defined in Lazr-JS. | 5 | use of widgets defined in Lazr-JS. |
663 | 6 | |||
664 | 7 | When rendering these widgets in page templates, all you need to do is 'call' | ||
665 | 8 | them. TAL will do this for you. | ||
666 | 9 | |||
667 | 10 | <tal:widget replace="structure view/nifty_widget"/> | ||
668 | 11 | |||
669 | 6 | 12 | ||
670 | 7 | TextLineEditorWidget | 13 | TextLineEditorWidget |
672 | 8 | -------------------------- | 14 | -------------------- |
673 | 9 | 15 | ||
674 | 10 | We have a convenient wrapper for the inlineedit/editor JS widget in | 16 | We have a convenient wrapper for the inlineedit/editor JS widget in |
675 | 11 | TextLineEditorWidget. | 17 | TextLineEditorWidget. |
676 | 12 | 18 | ||
687 | 13 | >>> from canonical.launchpad.webapp.publisher import canonical_url | 19 | >>> from lp.app.browser.lazrjs import TextLineEditorWidget |
688 | 14 | >>> from canonical.widgets.lazrjs import TextLineEditorWidget | 20 | |
689 | 15 | >>> bug = factory.makeBug(title='My bug is > important') | 21 | The bare minimum that you need to provide the widget is the object that you |
690 | 16 | 22 | are editing, and the exported field that is being edited, and a title for the | |
691 | 17 | The wrapper takes as arguments the object and the attribute of the | 23 | edit link that is rendered as the itle of the anchor so it shows on mouse |
692 | 18 | object that is being edited, as well as the URL to use for editing when | 24 | over, and the tag that surrounds the text. |
693 | 19 | when JS is turned off. | 25 | |
694 | 20 | 26 | >>> from lp.registry.interfaces.product import IProduct | |
695 | 21 | >>> widget = TextLineEditorWidget( | 27 | >>> product = factory.makeProduct( |
696 | 22 | ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit')) | 28 | ... name='widget', title='Widgets > important') |
697 | 29 | >>> title_field = IProduct['title'] | ||
698 | 30 | >>> title = 'Edit the title' | ||
699 | 31 | >>> widget = TextLineEditorWidget(product, title_field, title, 'h1') | ||
700 | 23 | 32 | ||
701 | 24 | The widget is rendered by executing it, it prints out the attribute | 33 | The widget is rendered by executing it, it prints out the attribute |
702 | 25 | content. | 34 | content. |
703 | 26 | 35 | ||
704 | 27 | >>> print widget() | 36 | >>> print widget() |
706 | 28 | <h1 id="..."><span class="yui3-editable_text-text">My bug is > | 37 | <h1 id="edit-title"><span class="yui3-editable_text-text">Widgets > |
707 | 29 | important</span> | 38 | important</span> |
708 | 30 | </h1> | 39 | </h1> |
709 | 31 | 40 | ||
710 | @@ -33,97 +42,229 @@ | |||
711 | 33 | the edit view that appears as well as a <script> tag that will change that | 42 | the edit view that appears as well as a <script> tag that will change that |
712 | 34 | link into an AJAX control when JS is available: | 43 | link into an AJAX control when JS is available: |
713 | 35 | 44 | ||
715 | 36 | >>> login('no-priv@canonical.com') | 45 | >>> login_person(product.owner) |
716 | 37 | >>> print widget() | 46 | >>> print widget() |
718 | 38 | <h1 id="..."><span class="yui3-editable_text-text">My bug is > | 47 | <h1 id="edit-title"><span class="yui3-editable_text-text">Widgets > |
719 | 39 | important</span> | 48 | important</span> |
723 | 40 | <a href="http://bugs.launchpad.dev/.../+edit" | 49 | <a class="yui3-editable_text-trigger sprite edit" |
724 | 41 | class="yui3-editable_text-trigger sprite edit" | 50 | href="http://launchpad.dev/widget/+edit"></a> |
722 | 42 | ></a> | ||
725 | 43 | </h1> | 51 | </h1> |
726 | 44 | <script> | 52 | <script> |
727 | 45 | ... | 53 | ... |
728 | 46 | </script> | 54 | </script> |
729 | 47 | 55 | ||
759 | 48 | The id and title attribute to use can be passed via parameters to the | 56 | |
760 | 49 | constructor: | 57 | Changing the tag |
761 | 50 | 58 | **************** | |
762 | 51 | >>> widget = TextLineEditorWidget( | 59 | |
763 | 52 | ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'), | 60 | The id of the surrounding tag defaults to "edit-" followed by the name of the |
764 | 53 | ... id="bug-title", title="Edit this summary") | 61 | attribute being edited. This can be overridden if needed using the |
765 | 54 | >>> print widget() | 62 | "content_box_id" constructor argument. |
766 | 55 | <h1 id="bug-title">... | 63 | |
767 | 56 | ...class="yui3-editable_text-trigger sprite edit"... | 64 | >>> span_widget = TextLineEditorWidget( |
768 | 57 | 65 | ... product, title_field, title, 'span', content_box_id="overridden") | |
769 | 58 | The initial_value_override parameter is passed as a JSON-serialized value. | 66 | >>> login(ANONYMOUS) # To not get the script tag rendered |
770 | 59 | 67 | >>> print span_widget() | |
771 | 60 | >>> widget = TextLineEditorWidget( | 68 | <span id="overridden">...</span> |
772 | 61 | ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'), | 69 | |
773 | 62 | ... id="bug-title", title="Edit this summary", | 70 | |
774 | 63 | ... initial_value_override='This bug has no title') | 71 | Changing the edit link |
775 | 64 | >>> print widget() | 72 | ********************** |
776 | 65 | <h1 id="bug-title">... | 73 | |
777 | 66 | ...initial_value_override: "This bug has no title"... | 74 | When there is a logged in user that has edit rights on the field being edited, |
778 | 67 | 75 | the edit button is shown, and has a link that takes to the user to a normal | |
779 | 68 | When the value isn't supplied it defaults to None, which is serialized as a | 76 | edit page if javascript is disabled. This link defaults to the '+edit' view |
780 | 69 | Javascript null. | 77 | of the object being edited. This can be overridden in two ways: |
781 | 70 | 78 | * change the 'edit_view' parameter to be a different view | |
782 | 71 | >>> widget = TextLineEditorWidget( | 79 | * provide an 'edit_url' to use instead |
783 | 72 | ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'), | 80 | |
784 | 73 | ... id="bug-title", title="Edit this summary") | 81 | >>> print widget.edit_url |
785 | 74 | >>> print widget() | 82 | http://launchpad.dev/widget/+edit |
786 | 75 | <h1 id="bug-title">... | 83 | |
787 | 76 | ...initial_value_override: null... | 84 | >>> diff_view = TextLineEditorWidget( |
788 | 85 | ... product, title_field, title, 'h1', edit_view='+edit-people') | ||
789 | 86 | >>> print diff_view.edit_url | ||
790 | 87 | http://launchpad.dev/widget/+edit-people | ||
791 | 88 | |||
792 | 89 | >>> diff_url = TextLineEditorWidget( | ||
793 | 90 | ... product, title_field, title, 'h1', edit_url='http://example.com/') | ||
794 | 91 | >>> print diff_url.edit_url | ||
795 | 92 | http://example.com/ | ||
796 | 93 | |||
797 | 94 | |||
798 | 95 | Other nifty bits | ||
799 | 96 | **************** | ||
800 | 97 | |||
801 | 98 | You are also able to set the default text to show if the attribute has no | ||
802 | 99 | value using the 'default_text' parameter. The 'initial_value_override' is | ||
803 | 100 | used by the javascript widget to provide that text instead of the objects | ||
804 | 101 | value (of the default_text). The width of the field can also be specified | ||
805 | 102 | using the 'width' parameter (please use 'em's). | ||
806 | 103 | |||
807 | 104 | For an example of these parameters, see the editor for a products programming | ||
808 | 105 | languages. | ||
809 | 106 | |||
810 | 107 | |||
811 | 108 | TextAreaEditorWidget | ||
812 | 109 | -------------------- | ||
813 | 110 | |||
814 | 111 | This widget renders a multi-line editor. Example uses of this widget are: | ||
815 | 112 | * editing a bug's description | ||
816 | 113 | * editing a merge proposal's commit message or description | ||
817 | 114 | * editing a PPA's description | ||
818 | 115 | |||
819 | 116 | >>> from lp.app.browser.lazrjs import TextAreaEditorWidget | ||
820 | 117 | |||
821 | 118 | The bare minimum that you need to provide the widget is the object that you | ||
822 | 119 | are editing, and the exported field that is being edited, and a title for the | ||
823 | 120 | edit link that is rendered as the itle of the anchor so it shows on mouse | ||
824 | 121 | over. | ||
825 | 122 | |||
826 | 123 | >>> eric = factory.makePerson(name='eric') | ||
827 | 124 | >>> archive = factory.makeArchive( | ||
828 | 125 | ... owner=eric, name='ppa', description='short description') | ||
829 | 126 | >>> from lp.soyuz.interfaces.archive import IArchive | ||
830 | 127 | >>> description = IArchive['description'] | ||
831 | 128 | >>> widget = TextAreaEditorWidget(archive, description, 'A title') | ||
832 | 129 | |||
833 | 130 | With no-one logged in, there are no edit buttons. | ||
834 | 131 | |||
835 | 132 | >>> print widget() | ||
836 | 133 | <div id="edit-description" class="lazr-multiline-edit"> | ||
837 | 134 | <div class="clearfix"> | ||
838 | 135 | <h2>A title</h2> | ||
839 | 136 | </div> | ||
840 | 137 | <div class="yui3-editable_text-text"><p>short description</p></div> | ||
841 | 138 | </div> | ||
842 | 139 | |||
843 | 140 | The initial text defaults to the value of the attribute, which is then passed | ||
844 | 141 | through two string formatter methods to obfuscate the email and then return | ||
845 | 142 | the text as HTML. | ||
846 | 143 | |||
847 | 144 | When the logged in user has edit permission, the edit button is shown, and | ||
848 | 145 | javascript is written to the page to hook up the links to show the multiline | ||
849 | 146 | editor. | ||
850 | 147 | |||
851 | 148 | >>> login_person(eric) | ||
852 | 149 | >>> print widget() | ||
853 | 150 | <div id="edit-description" class="lazr-multiline-edit"> | ||
854 | 151 | <div class="clearfix"> | ||
855 | 152 | <div class="edit-controls"> | ||
856 | 153 | | ||
857 | 154 | <a class="yui3-editable_text-trigger sprite edit" | ||
858 | 155 | href="http://launchpad.dev/~eric/+archive/ppa/+edit"></a> | ||
859 | 156 | </div> | ||
860 | 157 | <h2>A title</h2> | ||
861 | 158 | </div> | ||
862 | 159 | <div class="yui3-editable_text-text"><p>short description</p></div> | ||
863 | 160 | <script>...</script> | ||
864 | 161 | </div> | ||
865 | 162 | |||
866 | 163 | |||
867 | 164 | Changing the edit link | ||
868 | 165 | ********************** | ||
869 | 166 | |||
870 | 167 | The edit link can be changed in exactly the same way as for the | ||
871 | 168 | TextLineEditorWidget above. | ||
872 | 169 | |||
873 | 170 | |||
874 | 171 | Hiding the widget for empty fields | ||
875 | 172 | ********************************** | ||
876 | 173 | |||
877 | 174 | Sometimes you don't want to show the widget if there is no content. An | ||
878 | 175 | example of this can be found in the branch merge proposal view for editing the | ||
879 | 176 | description or the commit message. This uses links when there is no content. | ||
880 | 177 | Ideally the interaction with the links would be encoded as part of the widget | ||
881 | 178 | itself, but that is an exercise left for another yak shaver. | ||
882 | 179 | |||
883 | 180 | Hiding the widget is done by appending the "unseen" CSS class to the outer | ||
884 | 181 | tag. | ||
885 | 182 | |||
886 | 183 | >>> archive.description = None | ||
887 | 184 | >>> from lp.services.propertycache import clear_property_cache | ||
888 | 185 | >>> clear_property_cache(widget) | ||
889 | 186 | >>> print widget() | ||
890 | 187 | <div id="edit-description" class="lazr-multiline-edit unseen"> | ||
891 | 188 | ... | ||
892 | 189 | |||
893 | 190 | This behaviour can be overridden by setting the "hide_empty" parameter to | ||
894 | 191 | False. | ||
895 | 192 | |||
896 | 193 | >>> widget = TextAreaEditorWidget( | ||
897 | 194 | ... archive, description, 'A title', hide_empty=False) | ||
898 | 195 | >>> print widget() | ||
899 | 196 | <div id="edit-description" class="lazr-multiline-edit"> | ||
900 | 197 | ... | ||
901 | 198 | |||
902 | 199 | |||
903 | 200 | Not linkifying the text | ||
904 | 201 | *********************** | ||
905 | 202 | |||
906 | 203 | A part of the standard HTML rendering is to "linkify" links. That is, turn | ||
907 | 204 | words that look like hyperlinks into anchors. This is not always considered a | ||
908 | 205 | good idea as some spammers can create PPAs and link to other sites in the | ||
909 | 206 | descriptions. since the barrier to create a PPA is relatively low, we | ||
910 | 207 | restrict the linkability of some fields. The constructor provides a | ||
911 | 208 | "linkify_text" parameter that defaults to True. Set this to False to avoid | ||
912 | 209 | the linkification of text. See the IArchive['description'] editor for an example. | ||
913 | 77 | 210 | ||
914 | 78 | 211 | ||
915 | 79 | InlineEditPickerWidget | 212 | InlineEditPickerWidget |
916 | 80 | ---------------------- | 213 | ---------------------- |
917 | 81 | 214 | ||
966 | 82 | The InlineEditPickerWidget can be used for any interface attribute that | 215 | The InlineEditPickerWidget provides a simple way to create a popup selector |
967 | 83 | has a vocabulary defined for it. | 216 | widget to choose items from a vocabulary. |
968 | 84 | 217 | ||
969 | 85 | >>> from zope.component import getUtility | 218 | >>> from lp.app.browser.lazrjs import InlineEditPickerWidget |
970 | 86 | >>> from canonical.widgets.lazrjs import InlineEditPickerWidget | 219 | |
971 | 87 | >>> from lp.bugs.interfaces.bugtask import IBugTask, IBugTaskSet | 220 | The bare minimum that you need to provide the widget is the object that you |
972 | 88 | >>> bugtask = getUtility(IBugTaskSet).get(2) | 221 | are editing, and the exported field that is being edited, and the default |
973 | 89 | >>> def create_inline_edit_picker_widget(): | 222 | HTML representation of the field you are editing. |
974 | 90 | ... view = create_initialized_view(bugtask, '+index') | 223 | |
975 | 91 | ... return InlineEditPickerWidget( | 224 | Since most of the things that are being chosen are entities in Launchpad, and |
976 | 92 | ... context=view.context, | 225 | most of those entities have URLs, a common approach is to have the default |
977 | 93 | ... request=view.request, | 226 | HTML be a link to that entity. There is a utility function called format_link |
978 | 94 | ... interface_attribute=IBugTask['assignee'], | 227 | that does the equivalent of the TALES expression 'obj/fmt:link'. |
979 | 95 | ... default_html='default-html', | 228 | |
980 | 96 | ... header='Change assignee', | 229 | >>> from lp.app.browser.tales import format_link |
981 | 97 | ... step_title='Search for people or teams', | 230 | >>> default_text = format_link(archive.owner) |
982 | 98 | ... remove_button_text='Remove Assignee', | 231 | |
983 | 99 | ... null_display_value='Nobody') | 232 | The vocabulary is determined from the field passed in. If the vocabulary is a |
984 | 100 | 233 | huge vocabulary (one that provides a search), then the picker is shown with an | |
985 | 101 | An unauthenticated user cannot see the activator's edit button, which | 234 | entry field to allow the user to search for an item. If the vocabulary is not |
986 | 102 | is revealed by Y.lp.app.picker.addPickerPatcher. | 235 | huge, the different items are shown in the normal paginated way for the user |
987 | 103 | 236 | to select. | |
988 | 104 | >>> login(ANONYMOUS) | 237 | |
989 | 105 | >>> widget = create_inline_edit_picker_widget() | 238 | >>> owner = IArchive['owner'] |
990 | 106 | >>> print widget.can_write | 239 | >>> widget = InlineEditPickerWidget(archive, owner, default_text) |
991 | 107 | False | 240 | >>> print widget() |
992 | 108 | >>> print widget() | 241 | <span id="edit-owner"> |
993 | 109 | <span id="inline-picker-activator-id-...">... | 242 | <span class="yui3-activator-data-box"> |
994 | 110 | <div class="yui3-activator-message-box yui3-activator-hidden" /> | 243 | <a...>Eric</a> |
995 | 111 | </span> | 244 | </span> |
996 | 112 | 245 | <button class="lazr-btn yui3-activator-act yui3-activator-hidden"> | |
997 | 113 | The foo.bar user can see the activator's edit button. | 246 | Edit |
998 | 114 | 247 | </button> | |
999 | 115 | >>> login('foo.bar@canonical.com') | 248 | <div class="yui3-activator-message-box yui3-activator-hidden" /> |
1000 | 116 | >>> widget = create_inline_edit_picker_widget() | 249 | </span> |
1001 | 117 | >>> print widget.can_write | 250 | |
1002 | 118 | True | 251 | |
1003 | 119 | >>> print widget() | 252 | Picker headings |
1004 | 120 | <span id="inline-picker-activator-id-..."> | 253 | *************** |
1005 | 121 | ...<div class="yui3-activator-message-box yui3-activator-hidden" /> | 254 | |
1006 | 122 | </span> | 255 | The picker has two headings that are almost always desirable to customize. |
1007 | 123 | ...Y.lp.app.picker.addPickerPatcher(... | 256 | * "header" - Shown at the top of the picker |
1008 | 124 | 257 | * "step_title" - Shown just below the green progress bar | |
1009 | 125 | The json_resource_uri is the canonical_url for the object in | 258 | |
1010 | 126 | WebServiceClientRequests. | 259 | To customize these, pass the named parameters into the constructor of the |
1011 | 127 | 260 | widget. | |
1012 | 128 | >>> print widget.json_resource_uri | 261 | |
1013 | 129 | "/firefox/+bug/1" | 262 | |
1014 | 263 | Other nifty links | ||
1015 | 264 | ***************** | ||
1016 | 265 | |||
1017 | 266 | If the logged in user is in the defined vocabulary (only occurs with people | ||
1018 | 267 | type vocabularies), a link is shown "Assign me'. | ||
1019 | 268 | |||
1020 | 269 | If the field is optional, a "Remove" link is shown. The "Remove" text is | ||
1021 | 270 | customizable thought the "remove_button_text" parameter. | ||
1022 | 130 | 271 | ||
1023 | === modified file 'lib/lp/app/javascript/picker.js' | |||
1024 | --- lib/lp/app/javascript/picker.js 2011-01-20 21:26:07 +0000 | |||
1025 | +++ lib/lp/app/javascript/picker.js 2011-01-30 20:24:55 +0000 | |||
1026 | @@ -39,6 +39,7 @@ | |||
1027 | 39 | var remove_button_text = 'Remove'; | 39 | var remove_button_text = 'Remove'; |
1028 | 40 | var null_display_value = 'None'; | 40 | var null_display_value = 'None'; |
1029 | 41 | var show_search_box = true; | 41 | var show_search_box = true; |
1030 | 42 | resource_uri = LP.client.normalize_uri(resource_uri) | ||
1031 | 42 | var full_resource_uri = LP.client.get_absolute_uri(resource_uri); | 43 | var full_resource_uri = LP.client.get_absolute_uri(resource_uri); |
1032 | 43 | var current_context_uri = LP.client.cache['context']['self_link']; | 44 | var current_context_uri = LP.client.cache['context']['self_link']; |
1033 | 44 | var editing_main_context = (full_resource_uri == current_context_uri); | 45 | var editing_main_context = (full_resource_uri == current_context_uri); |
1034 | 45 | 46 | ||
1035 | === renamed file 'lib/canonical/widgets/templates/inline-picker.pt' => 'lib/lp/app/templates/inline-picker.pt' | |||
1036 | --- lib/canonical/widgets/templates/inline-picker.pt 2011-01-20 20:24:10 +0000 | |||
1037 | +++ lib/lp/app/templates/inline-picker.pt 2011-01-30 20:24:55 +0000 | |||
1038 | @@ -20,7 +20,7 @@ | |||
1039 | 20 | ${view/json_resource_uri}, | 20 | ${view/json_resource_uri}, |
1040 | 21 | ${view/json_attribute}, | 21 | ${view/json_attribute}, |
1041 | 22 | ${view/json_content_box_id}, | 22 | ${view/json_content_box_id}, |
1043 | 23 | ${view/config}); | 23 | ${view/json_config}); |
1044 | 24 | }, window); | 24 | }, window); |
1045 | 25 | }); | 25 | }); |
1046 | 26 | "/> | 26 | "/> |
1047 | 27 | 27 | ||
1048 | === added file 'lib/lp/app/templates/text-area-editor.pt' | |||
1049 | --- lib/lp/app/templates/text-area-editor.pt 1970-01-01 00:00:00 +0000 | |||
1050 | +++ lib/lp/app/templates/text-area-editor.pt 2011-01-30 20:24:55 +0000 | |||
1051 | @@ -0,0 +1,40 @@ | |||
1052 | 1 | <div tal:attributes="id view/content_box_id; | ||
1053 | 2 | class view/tag_class"> | ||
1054 | 3 | <div class="clearfix"> | ||
1055 | 4 | <div class="edit-controls" tal:condition="view/can_write"> | ||
1056 | 5 | | ||
1057 | 6 | <a tal:attributes="href view/edit_url" | ||
1058 | 7 | class="yui3-editable_text-trigger sprite edit"></a> | ||
1059 | 8 | </div> | ||
1060 | 9 | <h2 tal:condition="view/title" | ||
1061 | 10 | tal:content="view/title">the title</h2> | ||
1062 | 11 | </div> | ||
1063 | 12 | <div class="yui3-editable_text-text" | ||
1064 | 13 | tal:content="structure view/value">some text</div> | ||
1065 | 14 | <script tal:condition="view/can_write" | ||
1066 | 15 | tal:content="structure string: | ||
1067 | 16 | LPS.use('lazr.editor', 'lp.client.plugins', function (Y) { | ||
1068 | 17 | var widget = new Y.EditableText({ | ||
1069 | 18 | contentBox: ${view/widget_css_selector}, | ||
1070 | 19 | accept_empty: ${view/accept_empty}, | ||
1071 | 20 | multiline: true, | ||
1072 | 21 | buttons: 'top' | ||
1073 | 22 | }); | ||
1074 | 23 | widget.editor.plug({ | ||
1075 | 24 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { | ||
1076 | 25 | patch: ${view/json_attribute}, | ||
1077 | 26 | resource: ${view/json_attribute_uri}, | ||
1078 | 27 | patch_field: true, | ||
1079 | 28 | accept: 'application/xhtml+xml' | ||
1080 | 29 | }}); | ||
1081 | 30 | if (!Y.UA.opera) { | ||
1082 | 31 | widget.render(); | ||
1083 | 32 | } | ||
1084 | 33 | var lpns = Y.namespace('lp'); | ||
1085 | 34 | if (!lpns.widgets) { | ||
1086 | 35 | lpns.widgets = {}; | ||
1087 | 36 | } | ||
1088 | 37 | lpns.widgets['${view/content_box_id}'] = widget; | ||
1089 | 38 | }); | ||
1090 | 39 | "/> | ||
1091 | 40 | </div> | ||
1092 | 0 | 41 | ||
1093 | === added file 'lib/lp/app/templates/text-line-editor.pt' | |||
1094 | --- lib/lp/app/templates/text-line-editor.pt 1970-01-01 00:00:00 +0000 | |||
1095 | +++ lib/lp/app/templates/text-line-editor.pt 2011-01-30 20:24:55 +0000 | |||
1096 | @@ -0,0 +1,25 @@ | |||
1097 | 1 | <tal:open-tag replace="structure view/open_tag"/><span | ||
1098 | 2 | class="yui3-editable_text-text"><tal:text replace="view/value"/></span> | ||
1099 | 3 | <a tal:condition="view/can_write" | ||
1100 | 4 | tal:attributes="href view/edit_url" | ||
1101 | 5 | class="yui3-editable_text-trigger sprite edit"></a> | ||
1102 | 6 | <tal:close-tag replace="structure view/close_tag"/> | ||
1103 | 7 | |||
1104 | 8 | <script tal:condition="view/can_write" | ||
1105 | 9 | tal:content="structure string: | ||
1106 | 10 | LPS.use('lazr.editor', 'lp.client.plugins', function (Y) { | ||
1107 | 11 | var widget = new Y.EditableText({ | ||
1108 | 12 | contentBox: ${view/widget_css_selector}, | ||
1109 | 13 | accept_empty: ${view/accept_empty}, | ||
1110 | 14 | width: ${view/width}, | ||
1111 | 15 | initial_value_override: ${view/initial_value_override} | ||
1112 | 16 | }); | ||
1113 | 17 | widget.editor.plug({ | ||
1114 | 18 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { | ||
1115 | 19 | patch: ${view/json_attribute}, | ||
1116 | 20 | resource: ${view/json_attribute_uri}, | ||
1117 | 21 | patch_field: true | ||
1118 | 22 | }}); | ||
1119 | 23 | widget.render(); | ||
1120 | 24 | }); | ||
1121 | 25 | "/> | ||
1122 | 0 | 26 | ||
1123 | === modified file 'lib/lp/bugs/browser/bugtask.py' | |||
1124 | --- lib/lp/bugs/browser/bugtask.py 2011-01-18 20:29:21 +0000 | |||
1125 | +++ lib/lp/bugs/browser/bugtask.py 2011-01-30 20:24:55 +0000 | |||
1126 | @@ -163,11 +163,6 @@ | |||
1127 | 163 | from canonical.lazr.interfaces import IObjectPrivacy | 163 | from canonical.lazr.interfaces import IObjectPrivacy |
1128 | 164 | from canonical.lazr.utils import smartquote | 164 | from canonical.lazr.utils import smartquote |
1129 | 165 | from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget | 165 | from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget |
1130 | 166 | from canonical.widgets.lazrjs import ( | ||
1131 | 167 | TextAreaEditorWidget, | ||
1132 | 168 | TextLineEditorWidget, | ||
1133 | 169 | vocabulary_to_choice_edit_items, | ||
1134 | 170 | ) | ||
1135 | 171 | from canonical.widgets.project import ProjectScopeWidget | 166 | from canonical.widgets.project import ProjectScopeWidget |
1136 | 172 | from lp.answers.interfaces.questiontarget import IQuestionTarget | 167 | from lp.answers.interfaces.questiontarget import IQuestionTarget |
1137 | 173 | from lp.app.browser.launchpadform import ( | 168 | from lp.app.browser.launchpadform import ( |
1138 | @@ -176,8 +171,12 @@ | |||
1139 | 176 | LaunchpadEditFormView, | 171 | LaunchpadEditFormView, |
1140 | 177 | LaunchpadFormView, | 172 | LaunchpadFormView, |
1141 | 178 | ) | 173 | ) |
1142 | 174 | from lp.app.browser.lazrjs import ( | ||
1143 | 175 | TextAreaEditorWidget, | ||
1144 | 176 | TextLineEditorWidget, | ||
1145 | 177 | vocabulary_to_choice_edit_items, | ||
1146 | 178 | ) | ||
1147 | 179 | from lp.app.browser.tales import ( | 179 | from lp.app.browser.tales import ( |
1148 | 180 | FormattersAPI, | ||
1149 | 181 | ObjectImageDisplayAPI, | 180 | ObjectImageDisplayAPI, |
1150 | 182 | PersonFormatterAPI, | 181 | PersonFormatterAPI, |
1151 | 183 | ) | 182 | ) |
1152 | @@ -679,8 +678,8 @@ | |||
1153 | 679 | canonical_url(self.context.bug.default_bugtask)) | 678 | canonical_url(self.context.bug.default_bugtask)) |
1154 | 680 | 679 | ||
1155 | 681 | self.bug_title_edit_widget = TextLineEditorWidget( | 680 | self.bug_title_edit_widget = TextLineEditorWidget( |
1158 | 682 | bug, 'title', canonical_url(self.context, view_name='+edit'), | 681 | bug, IBug['title'], "Edit this summary", 'h1', |
1159 | 683 | id="bug-title", title="Edit this summary") | 682 | edit_url=canonical_url(self.context, view_name='+edit')) |
1160 | 684 | 683 | ||
1161 | 685 | # XXX 2010-10-05 gmb bug=655597: | 684 | # XXX 2010-10-05 gmb bug=655597: |
1162 | 686 | # This line of code keeps the view's query count down, | 685 | # This line of code keeps the view's query count down, |
1163 | @@ -1035,16 +1034,12 @@ | |||
1164 | 1035 | @property | 1034 | @property |
1165 | 1036 | def bug_description_html(self): | 1035 | def bug_description_html(self): |
1166 | 1037 | """The bug's description as HTML.""" | 1036 | """The bug's description as HTML.""" |
1170 | 1038 | formatter = FormattersAPI | 1037 | bug = self.context.bug |
1171 | 1039 | hide_email = formatter(self.context.bug.description).obfuscate_email() | 1038 | description = IBug['description'] |
1172 | 1040 | description = formatter(hide_email).text_to_html() | 1039 | title = "Bug Description" |
1173 | 1040 | edit_url = canonical_url(self.context, view_name='+edit') | ||
1174 | 1041 | return TextAreaEditorWidget( | 1041 | return TextAreaEditorWidget( |
1181 | 1042 | self.context.bug, | 1042 | bug, description, title, edit_url=edit_url) |
1176 | 1043 | 'description', | ||
1177 | 1044 | canonical_url(self.context, view_name='+edit'), | ||
1178 | 1045 | id="edit-description", | ||
1179 | 1046 | title="Bug Description", | ||
1180 | 1047 | value=description) | ||
1182 | 1048 | 1043 | ||
1183 | 1049 | @property | 1044 | @property |
1184 | 1050 | def bug_heat_html(self): | 1045 | def bug_heat_html(self): |
1185 | 1051 | 1046 | ||
1186 | === modified file 'lib/lp/bugs/templates/bugtask-index.pt' | |||
1187 | --- lib/lp/bugs/templates/bugtask-index.pt 2010-12-21 15:07:26 +0000 | |||
1188 | +++ lib/lp/bugs/templates/bugtask-index.pt 2011-01-30 20:24:55 +0000 | |||
1189 | @@ -103,9 +103,7 @@ | |||
1190 | 103 | <tal:description | 103 | <tal:description |
1191 | 104 | define="global description context/bug/description/fmt:obfuscate-email/fmt:text-to-html" /> | 104 | define="global description context/bug/description/fmt:obfuscate-email/fmt:text-to-html" /> |
1192 | 105 | 105 | ||
1196 | 106 | <div id="edit-description" class="lazr-multiline-edit" | 106 | <tal:widget replace="structure view/bug_description_html"/> |
1194 | 107 | tal:content="structure view/bug_description_html" | ||
1195 | 108 | /> | ||
1197 | 109 | 107 | ||
1198 | 110 | <div style="margin:-20px 0 20px 5px" class="clearfix"> | 108 | <div style="margin:-20px 0 20px 5px" class="clearfix"> |
1199 | 111 | <span tal:condition="view/wasDescriptionModified" class="discreet" | 109 | <span tal:condition="view/wasDescriptionModified" class="discreet" |
1200 | 112 | 110 | ||
1201 | === modified file 'lib/lp/bugs/windmill/tests/test_mark_duplicate.py' | |||
1202 | --- lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-11-23 14:18:07 +0000 | |||
1203 | +++ lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2011-01-30 20:24:55 +0000 | |||
1204 | @@ -115,7 +115,7 @@ | |||
1205 | 115 | client.click(link=u'bug #1') | 115 | client.click(link=u'bug #1') |
1206 | 116 | client.waits.forPageLoad(timeout=constants.PAGE_LOAD) | 116 | client.waits.forPageLoad(timeout=constants.PAGE_LOAD) |
1207 | 117 | client.waits.forElement( | 117 | client.waits.forElement( |
1209 | 118 | id=u'bug-title', timeout=constants.FOR_ELEMENT) | 118 | id=u'edit-title', timeout=constants.FOR_ELEMENT) |
1210 | 119 | 119 | ||
1211 | 120 | # Make sure all js loads are complete before trying the next test. | 120 | # Make sure all js loads are complete before trying the next test. |
1212 | 121 | client.waits.forElement( | 121 | client.waits.forElement( |
1213 | 122 | 122 | ||
1214 | === modified file 'lib/lp/code/browser/branch.py' | |||
1215 | --- lib/lp/code/browser/branch.py 2011-01-28 02:16:33 +0000 | |||
1216 | +++ lib/lp/code/browser/branch.py 2011-01-30 20:24:55 +0000 | |||
1217 | @@ -100,13 +100,13 @@ | |||
1218 | 100 | from canonical.lazr.utils import smartquote | 100 | from canonical.lazr.utils import smartquote |
1219 | 101 | from canonical.widgets.suggestion import TargetBranchWidget | 101 | from canonical.widgets.suggestion import TargetBranchWidget |
1220 | 102 | from canonical.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription | 102 | from canonical.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription |
1221 | 103 | from canonical.widgets.lazrjs import vocabulary_to_choice_edit_items | ||
1222 | 104 | from lp.app.browser.launchpadform import ( | 103 | from lp.app.browser.launchpadform import ( |
1223 | 105 | action, | 104 | action, |
1224 | 106 | custom_widget, | 105 | custom_widget, |
1225 | 107 | LaunchpadEditFormView, | 106 | LaunchpadEditFormView, |
1226 | 108 | LaunchpadFormView, | 107 | LaunchpadFormView, |
1227 | 109 | ) | 108 | ) |
1228 | 109 | from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items | ||
1229 | 110 | from lp.app.errors import NotFoundError | 110 | from lp.app.errors import NotFoundError |
1230 | 111 | from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch | 111 | from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch |
1231 | 112 | from lp.bugs.interfaces.bug import IBugSet | 112 | from lp.bugs.interfaces.bug import IBugSet |
1232 | 113 | 113 | ||
1233 | === modified file 'lib/lp/code/browser/branchmergeproposal.py' | |||
1234 | --- lib/lp/code/browser/branchmergeproposal.py 2011-01-18 20:49:35 +0000 | |||
1235 | +++ lib/lp/code/browser/branchmergeproposal.py 2011-01-30 20:24:55 +0000 | |||
1236 | @@ -87,12 +87,11 @@ | |||
1237 | 87 | LaunchpadEditFormView, | 87 | LaunchpadEditFormView, |
1238 | 88 | LaunchpadFormView, | 88 | LaunchpadFormView, |
1239 | 89 | ) | 89 | ) |
1242 | 90 | from lp.app.browser.tales import DateTimeFormatterAPI | 90 | from lp.app.browser.lazrjs import ( |
1241 | 91 | from canonical.widgets.lazrjs import ( | ||
1243 | 92 | TextAreaEditorWidget, | 91 | TextAreaEditorWidget, |
1244 | 93 | vocabulary_to_choice_edit_items, | 92 | vocabulary_to_choice_edit_items, |
1245 | 94 | ) | 93 | ) |
1247 | 95 | from lp.app.browser.stringformatter import FormattersAPI | 94 | from lp.app.browser.tales import DateTimeFormatterAPI |
1248 | 96 | from lp.code.adapters.branch import BranchMergeProposalDelta | 95 | from lp.code.adapters.branch import BranchMergeProposalDelta |
1249 | 97 | from lp.code.browser.codereviewcomment import CodeReviewDisplayComment | 96 | from lp.code.browser.codereviewcomment import CodeReviewDisplayComment |
1250 | 98 | from lp.code.browser.decorations import ( | 97 | from lp.code.browser.decorations import ( |
1251 | @@ -691,40 +690,36 @@ | |||
1252 | 691 | for bug in self.context.related_bugs] | 690 | for bug in self.context.related_bugs] |
1253 | 692 | 691 | ||
1254 | 693 | @property | 692 | @property |
1255 | 693 | def edit_description_link_class(self): | ||
1256 | 694 | if self.context.description: | ||
1257 | 695 | return "unseen" | ||
1258 | 696 | else: | ||
1259 | 697 | return "" | ||
1260 | 698 | |||
1261 | 699 | @property | ||
1262 | 694 | def description_html(self): | 700 | def description_html(self): |
1263 | 695 | """The description as widget HTML.""" | 701 | """The description as widget HTML.""" |
1270 | 696 | description = self.context.description | 702 | mp = self.context |
1271 | 697 | if description is None: | 703 | description = IBranchMergeProposal['description'] |
1272 | 698 | description = '' | 704 | title = "Description of the Change" |
1267 | 699 | formatter = FormattersAPI | ||
1268 | 700 | hide_email = formatter(description).obfuscate_email() | ||
1269 | 701 | description = formatter(hide_email).text_to_html() | ||
1273 | 702 | return TextAreaEditorWidget( | 705 | return TextAreaEditorWidget( |
1281 | 703 | self.context, | 706 | mp, description, title, edit_view='+edit-description') |
1282 | 704 | 'description', | 707 | |
1283 | 705 | canonical_url(self.context, view_name='+edit-description'), | 708 | @property |
1284 | 706 | id="edit-description", | 709 | def edit_commit_message_link_class(self): |
1285 | 707 | title="Description of the Change", | 710 | if self.context.commit_message: |
1286 | 708 | value=description, | 711 | return "unseen" |
1287 | 709 | accept_empty=True) | 712 | else: |
1288 | 713 | return "" | ||
1289 | 710 | 714 | ||
1290 | 711 | @property | 715 | @property |
1291 | 712 | def commit_message_html(self): | 716 | def commit_message_html(self): |
1292 | 713 | """The commit message as widget HTML.""" | 717 | """The commit message as widget HTML.""" |
1299 | 714 | commit_message = self.context.commit_message | 718 | mp = self.context |
1300 | 715 | if commit_message is None: | 719 | commit_message = IBranchMergeProposal['commit_message'] |
1301 | 716 | commit_message = '' | 720 | title = "Commit Message" |
1296 | 717 | formatter = FormattersAPI | ||
1297 | 718 | hide_email = formatter(commit_message).obfuscate_email() | ||
1298 | 719 | commit_message = formatter(hide_email).text_to_html() | ||
1302 | 720 | return TextAreaEditorWidget( | 721 | return TextAreaEditorWidget( |
1310 | 721 | self.context, | 722 | mp, commit_message, title, edit_view='+edit-commit-message') |
1304 | 722 | 'commit_message', | ||
1305 | 723 | canonical_url(self.context, view_name='+edit-commit-message'), | ||
1306 | 724 | id="edit-commit_message", | ||
1307 | 725 | title="Commit Message", | ||
1308 | 726 | value=commit_message, | ||
1309 | 727 | accept_empty=True) | ||
1311 | 728 | 723 | ||
1312 | 729 | @property | 724 | @property |
1313 | 730 | def status_config(self): | 725 | def status_config(self): |
1314 | 731 | 726 | ||
1315 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' | |||
1316 | --- lib/lp/code/browser/sourcepackagerecipe.py 2011-01-20 20:50:45 +0000 | |||
1317 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2011-01-30 20:24:55 +0000 | |||
1318 | @@ -62,7 +62,6 @@ | |||
1319 | 62 | LabeledMultiCheckBoxWidget, | 62 | LabeledMultiCheckBoxWidget, |
1320 | 63 | LaunchpadRadioWidget, | 63 | LaunchpadRadioWidget, |
1321 | 64 | ) | 64 | ) |
1322 | 65 | from canonical.widgets.lazrjs import InlineEditPickerWidget | ||
1323 | 66 | from canonical.widgets.suggestion import RecipeOwnerWidget | 65 | from canonical.widgets.suggestion import RecipeOwnerWidget |
1324 | 67 | from lp.app.browser.launchpadform import ( | 66 | from lp.app.browser.launchpadform import ( |
1325 | 68 | action, | 67 | action, |
1326 | @@ -72,9 +71,8 @@ | |||
1327 | 72 | LaunchpadFormView, | 71 | LaunchpadFormView, |
1328 | 73 | render_radio_widget_part, | 72 | render_radio_widget_part, |
1329 | 74 | ) | 73 | ) |
1333 | 75 | from lp.app.browser.tales import ( | 74 | from lp.app.browser.lazrjs import InlineEditPickerWidget |
1334 | 76 | format_link, | 75 | from lp.app.browser.tales import format_link |
1332 | 77 | ) | ||
1335 | 78 | from lp.code.errors import ( | 76 | from lp.code.errors import ( |
1336 | 79 | BuildAlreadyPending, | 77 | BuildAlreadyPending, |
1337 | 80 | NoSuchBranch, | 78 | NoSuchBranch, |
1338 | @@ -235,9 +233,8 @@ | |||
1339 | 235 | @property | 233 | @property |
1340 | 236 | def person_picker(self): | 234 | def person_picker(self): |
1341 | 237 | return InlineEditPickerWidget( | 235 | return InlineEditPickerWidget( |
1343 | 238 | self.context, self.request, ISourcePackageRecipe['owner'], | 236 | self.context, ISourcePackageRecipe['owner'], |
1344 | 239 | format_link(self.context.owner), | 237 | format_link(self.context.owner), |
1345 | 240 | content_box_id='recipe-owner', | ||
1346 | 241 | header='Change owner', | 238 | header='Change owner', |
1347 | 242 | step_title='Select a new owner') | 239 | step_title='Select a new owner') |
1348 | 243 | 240 | ||
1349 | @@ -248,11 +245,9 @@ | |||
1350 | 248 | initial_html = 'None' | 245 | initial_html = 'None' |
1351 | 249 | else: | 246 | else: |
1352 | 250 | initial_html = format_link(ppa) | 247 | initial_html = format_link(ppa) |
1353 | 248 | field = ISourcePackageEditSchema['daily_build_archive'] | ||
1354 | 251 | return InlineEditPickerWidget( | 249 | return InlineEditPickerWidget( |
1359 | 252 | self.context, self.request, | 250 | self.context, field, initial_html, |
1356 | 253 | ISourcePackageAddSchema['daily_build_archive'], | ||
1357 | 254 | initial_html, | ||
1358 | 255 | content_box_id='recipe-ppa', | ||
1360 | 256 | header='Change daily build archive', | 251 | header='Change daily build archive', |
1361 | 257 | step_title='Select a PPA') | 252 | step_title='Select a PPA') |
1362 | 258 | 253 | ||
1363 | 259 | 254 | ||
1364 | === modified file 'lib/lp/code/templates/branchmergeproposal-index.pt' | |||
1365 | --- lib/lp/code/templates/branchmergeproposal-index.pt 2010-10-10 21:54:16 +0000 | |||
1366 | +++ lib/lp/code/templates/branchmergeproposal-index.pt 2011-01-30 20:24:55 +0000 | |||
1367 | @@ -95,45 +95,23 @@ | |||
1368 | 95 | </div> | 95 | </div> |
1369 | 96 | 96 | ||
1370 | 97 | <div id="commit-message" class="yui-g"> | 97 | <div id="commit-message" class="yui-g"> |
1389 | 98 | <tal:no-commit-message condition="not: context/commit_message"> | 98 | <div tal:define="link menu/set_commit_message" |
1390 | 99 | <div tal:define="link menu/set_commit_message" | 99 | tal:condition="link/enabled" |
1391 | 100 | tal:condition="link/enabled" | 100 | tal:content="structure link/render" |
1392 | 101 | tal:content="structure link/render"> | 101 | tal:attributes="class view/edit_commit_message_link_class"> |
1393 | 102 | Set commit message | 102 | Set commit message |
1394 | 103 | </div> | 103 | </div> |
1395 | 104 | <div id="edit-commit_message" class="lazr-multiline-edit unseen" | 104 | <tal:widget replace="structure view/commit_message_html"/> |
1378 | 105 | tal:content="structure view/commit_message_html"/> | ||
1379 | 106 | </tal:no-commit-message> | ||
1380 | 107 | <tal:has-commit-message condition="context/commit_message"> | ||
1381 | 108 | <div tal:define="link menu/set_commit_message" | ||
1382 | 109 | tal:condition="link/enabled" | ||
1383 | 110 | tal:content="structure link/render" class="unseen"> | ||
1384 | 111 | Set commit message | ||
1385 | 112 | </div> | ||
1386 | 113 | <div id="edit-commit_message" class="lazr-multiline-edit" | ||
1387 | 114 | tal:content="structure view/commit_message_html"/> | ||
1388 | 115 | </tal:has-commit-message> | ||
1396 | 116 | </div> | 105 | </div> |
1397 | 117 | 106 | ||
1398 | 118 | <div id="description" class="yui-g"> | 107 | <div id="description" class="yui-g"> |
1417 | 119 | <tal:no-description condition="not: context/description"> | 108 | <div tal:define="link menu/set_description" |
1418 | 120 | <div tal:define="link menu/set_description" | 109 | tal:condition="link/enabled" |
1419 | 121 | tal:condition="link/enabled" | 110 | tal:content="structure link/render" |
1420 | 122 | tal:content="structure link/render"> | 111 | tal:attributes="class view/edit_description_link_class"> |
1421 | 123 | Set description | 112 | Set description |
1422 | 124 | </div> | 113 | </div> |
1423 | 125 | <div id="edit-description" class="lazr-multiline-edit unseen" | 114 | <tal:widget replace="structure view/description_html"/> |
1406 | 126 | tal:content="structure view/description_html"/> | ||
1407 | 127 | </tal:no-description> | ||
1408 | 128 | <tal:has-description condition="context/description"> | ||
1409 | 129 | <div tal:define="link menu/set_description" | ||
1410 | 130 | tal:condition="link/enabled" | ||
1411 | 131 | tal:content="structure link/render" class="unseen"> | ||
1412 | 132 | Set description | ||
1413 | 133 | </div> | ||
1414 | 134 | <div id="edit-description" class="lazr-multiline-edit" | ||
1415 | 135 | tal:content="structure view/description_html"/> | ||
1416 | 136 | </tal:has-description> | ||
1424 | 137 | </div> | 115 | </div> |
1425 | 138 | 116 | ||
1426 | 139 | <div class="yui-g" tal:condition="view/has_bug_or_spec"> | 117 | <div class="yui-g" tal:condition="view/has_bug_or_spec"> |
1427 | 140 | 118 | ||
1428 | === modified file 'lib/lp/registry/browser/product.py' | |||
1429 | --- lib/lp/registry/browser/product.py 2011-01-20 21:41:13 +0000 | |||
1430 | +++ lib/lp/registry/browser/product.py 2011-01-30 20:24:55 +0000 | |||
1431 | @@ -122,7 +122,6 @@ | |||
1432 | 122 | CheckBoxMatrixWidget, | 122 | CheckBoxMatrixWidget, |
1433 | 123 | LaunchpadRadioWidget, | 123 | LaunchpadRadioWidget, |
1434 | 124 | ) | 124 | ) |
1435 | 125 | from canonical.widgets.lazrjs import TextLineEditorWidget | ||
1436 | 126 | from canonical.widgets.popup import PersonPickerWidget | 125 | from canonical.widgets.popup import PersonPickerWidget |
1437 | 127 | from canonical.widgets.product import ( | 126 | from canonical.widgets.product import ( |
1438 | 128 | GhostWidget, | 127 | GhostWidget, |
1439 | @@ -143,6 +142,7 @@ | |||
1440 | 143 | ReturnToReferrerMixin, | 142 | ReturnToReferrerMixin, |
1441 | 144 | safe_action, | 143 | safe_action, |
1442 | 145 | ) | 144 | ) |
1443 | 145 | from lp.app.browser.lazrjs import TextLineEditorWidget | ||
1444 | 146 | from lp.app.browser.tales import MenuAPI | 146 | from lp.app.browser.tales import MenuAPI |
1445 | 147 | from lp.app.enums import ServiceUsage | 147 | from lp.app.enums import ServiceUsage |
1446 | 148 | from lp.app.errors import NotFoundError | 148 | from lp.app.errors import NotFoundError |
1447 | @@ -988,25 +988,21 @@ | |||
1448 | 988 | 988 | ||
1449 | 989 | def initialize(self): | 989 | def initialize(self): |
1450 | 990 | self.status_message = None | 990 | self.status_message = None |
1451 | 991 | product = self.context | ||
1452 | 992 | title_field = IProduct['title'] | ||
1453 | 993 | title = "Edit this title" | ||
1454 | 991 | self.title_edit_widget = TextLineEditorWidget( | 994 | self.title_edit_widget = TextLineEditorWidget( |
1458 | 992 | self.context, 'title', | 995 | product, title_field, title, 'h1') |
1459 | 993 | canonical_url(self.context, view_name='+edit'), | 996 | programming_lang = IProduct['programminglang'] |
1460 | 994 | id="product-title", title="Edit this title") | 997 | title = 'Edit programming languages' |
1461 | 998 | additional_arguments = {'width': '9em'} | ||
1462 | 995 | if self.context.programminglang is None: | 999 | if self.context.programminglang is None: |
1464 | 996 | additional_arguments = dict( | 1000 | additional_arguments.update(dict( |
1465 | 997 | default_text='Not yet specified', | 1001 | default_text='Not yet specified', |
1466 | 998 | initial_value_override='', | 1002 | initial_value_override='', |
1470 | 999 | ) | 1003 | )) |
1468 | 1000 | else: | ||
1469 | 1001 | additional_arguments = {} | ||
1471 | 1002 | self.languages_edit_widget = TextLineEditorWidget( | 1004 | self.languages_edit_widget = TextLineEditorWidget( |
1479 | 1003 | self.context, 'programminglang', | 1005 | product, programming_lang, title, 'span', **additional_arguments) |
1473 | 1004 | canonical_url(self.context, view_name='+edit'), | ||
1474 | 1005 | id='programminglang', title='Edit programming languages', | ||
1475 | 1006 | tag='span', public_attribute='programming_language', | ||
1476 | 1007 | accept_empty=True, | ||
1477 | 1008 | width='9em', | ||
1478 | 1009 | **additional_arguments) | ||
1480 | 1010 | self.show_programming_languages = bool( | 1006 | self.show_programming_languages = bool( |
1481 | 1011 | self.context.programminglang or | 1007 | self.context.programminglang or |
1482 | 1012 | check_permission('launchpad.Edit', self.context)) | 1008 | check_permission('launchpad.Edit', self.context)) |
1483 | 1013 | 1009 | ||
1484 | === modified file 'lib/lp/registry/windmill/tests/test_product.py' | |||
1485 | --- lib/lp/registry/windmill/tests/test_product.py 2010-10-18 12:56:47 +0000 | |||
1486 | +++ lib/lp/registry/windmill/tests/test_product.py 2011-01-30 20:24:55 +0000 | |||
1487 | @@ -24,7 +24,7 @@ | |||
1488 | 24 | def test_title_inline_edit(self): | 24 | def test_title_inline_edit(self): |
1489 | 25 | test = widgets.InlineEditorWidgetTest( | 25 | test = widgets.InlineEditorWidgetTest( |
1490 | 26 | url='%s/firefox' % RegistryWindmillLayer.base_url, | 26 | url='%s/firefox' % RegistryWindmillLayer.base_url, |
1492 | 27 | widget_id='product-title', | 27 | widget_id='edit-title', |
1493 | 28 | expected_value='Mozilla Firefox', | 28 | expected_value='Mozilla Firefox', |
1494 | 29 | new_value='The awesome Mozilla Firefox', | 29 | new_value='The awesome Mozilla Firefox', |
1495 | 30 | name='test_title_inline_edit', | 30 | name='test_title_inline_edit', |
1496 | @@ -35,7 +35,7 @@ | |||
1497 | 35 | def test_programming_languages_edit(self): | 35 | def test_programming_languages_edit(self): |
1498 | 36 | test = widgets.InlineEditorWidgetTest( | 36 | test = widgets.InlineEditorWidgetTest( |
1499 | 37 | url='%s/firefox' % RegistryWindmillLayer.base_url, | 37 | url='%s/firefox' % RegistryWindmillLayer.base_url, |
1501 | 38 | widget_id='programminglang', | 38 | widget_id='edit-programminglang', |
1502 | 39 | widget_tag='span', | 39 | widget_tag='span', |
1503 | 40 | expected_value='Not yet specified', | 40 | expected_value='Not yet specified', |
1504 | 41 | new_value='C++', | 41 | new_value='C++', |
1505 | 42 | 42 | ||
1506 | === modified file 'lib/lp/soyuz/browser/archive.py' | |||
1507 | --- lib/lp/soyuz/browser/archive.py 2011-01-29 07:41:50 +0000 | |||
1508 | +++ lib/lp/soyuz/browser/archive.py 2011-01-30 20:24:55 +0000 | |||
1509 | @@ -87,10 +87,6 @@ | |||
1510 | 87 | LaunchpadDropdownWidget, | 87 | LaunchpadDropdownWidget, |
1511 | 88 | LaunchpadRadioWidget, | 88 | LaunchpadRadioWidget, |
1512 | 89 | ) | 89 | ) |
1513 | 90 | from canonical.widgets.lazrjs import ( | ||
1514 | 91 | TextAreaEditorWidget, | ||
1515 | 92 | TextLineEditorWidget, | ||
1516 | 93 | ) | ||
1517 | 94 | from canonical.widgets.textwidgets import StrippedTextWidget | 90 | from canonical.widgets.textwidgets import StrippedTextWidget |
1518 | 95 | from lp.app.browser.launchpadform import ( | 91 | from lp.app.browser.launchpadform import ( |
1519 | 96 | action, | 92 | action, |
1520 | @@ -98,6 +94,10 @@ | |||
1521 | 98 | LaunchpadEditFormView, | 94 | LaunchpadEditFormView, |
1522 | 99 | LaunchpadFormView, | 95 | LaunchpadFormView, |
1523 | 100 | ) | 96 | ) |
1524 | 97 | from lp.app.browser.lazrjs import ( | ||
1525 | 98 | TextAreaEditorWidget, | ||
1526 | 99 | TextLineEditorWidget, | ||
1527 | 100 | ) | ||
1528 | 101 | from lp.app.browser.stringformatter import FormattersAPI | 101 | from lp.app.browser.stringformatter import FormattersAPI |
1529 | 102 | from lp.app.errors import NotFoundError | 102 | from lp.app.errors import NotFoundError |
1530 | 103 | from lp.buildmaster.enums import BuildStatus | 103 | from lp.buildmaster.enums import BuildStatus |
1531 | @@ -863,11 +863,9 @@ | |||
1532 | 863 | 863 | ||
1533 | 864 | @property | 864 | @property |
1534 | 865 | def displayname_edit_widget(self): | 865 | def displayname_edit_widget(self): |
1540 | 866 | widget = TextLineEditorWidget( | 866 | display_name = IArchive['displayname'] |
1541 | 867 | self.context, 'displayname', | 867 | title = "Edit the displayname" |
1542 | 868 | canonical_url(self.context, view_name='+edit'), | 868 | return TextLineEditorWidget(self.context, display_name, title, 'h1') |
1538 | 869 | id="displayname", title="Edit the displayname") | ||
1539 | 870 | return widget | ||
1543 | 871 | 869 | ||
1544 | 872 | @property | 870 | @property |
1545 | 873 | def sources_list_entries(self): | 871 | def sources_list_entries(self): |
1546 | @@ -897,25 +895,17 @@ | |||
1547 | 897 | @property | 895 | @property |
1548 | 898 | def archive_description_html(self): | 896 | def archive_description_html(self): |
1549 | 899 | """The archive's description as HTML.""" | 897 | """The archive's description as HTML.""" |
1558 | 900 | formatter = FormattersAPI | 898 | linkify_text = True |
1551 | 901 | |||
1552 | 902 | description = self.context.description | ||
1553 | 903 | if description is not None: | ||
1554 | 904 | description = formatter(description).obfuscate_email() | ||
1555 | 905 | else: | ||
1556 | 906 | description = '' | ||
1557 | 907 | |||
1559 | 908 | if self.context.is_ppa: | 899 | if self.context.is_ppa: |
1563 | 909 | description = formatter(description).text_to_html( | 900 | linkify_text = not self.context.owner.is_probationary |
1564 | 910 | linkify_text=(not self.context.owner.is_probationary)) | 901 | archive = self.context |
1565 | 911 | 902 | description = IArchive['description'] | |
1566 | 903 | title = self.archive_label + " description" | ||
1567 | 904 | # Don't hide empty archive descriptions. Even though the interface | ||
1568 | 905 | # says they are required, the model doesn't. | ||
1569 | 912 | return TextAreaEditorWidget( | 906 | return TextAreaEditorWidget( |
1576 | 913 | self.context, | 907 | archive, description, title, hide_empty=False, |
1577 | 914 | 'description', | 908 | linkify_text=linkify_text) |
1572 | 915 | canonical_url(self.context, view_name='+edit'), | ||
1573 | 916 | id="edit-description", | ||
1574 | 917 | title=self.archive_label + " description", | ||
1575 | 918 | value=description) | ||
1578 | 919 | 909 | ||
1579 | 920 | @cachedproperty | 910 | @cachedproperty |
1580 | 921 | def latest_updates(self): | 911 | def latest_updates(self): |
1581 | 922 | 912 | ||
1582 | === modified file 'lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt' | |||
1583 | --- lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2010-10-17 15:44:08 +0000 | |||
1584 | +++ lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2011-01-30 20:24:55 +0000 | |||
1585 | @@ -199,8 +199,9 @@ | |||
1586 | 199 | Copy archive disabled-security-rebuild for No Privileges Person : Ubuntu | 199 | Copy archive disabled-security-rebuild for No Privileges Person : Ubuntu |
1587 | 200 | 200 | ||
1588 | 201 | >>> main_content = find_main_content(nopriv_browser.contents) | 201 | >>> main_content = find_main_content(nopriv_browser.contents) |
1591 | 202 | >>> print main_content.h1 | 202 | >>> print main_content.h1['class'] |
1592 | 203 | <h1 id="displayname">...</h1> | 203 | Traceback (most recent call last): |
1593 | 204 | KeyError: 'class' | ||
1594 | 204 | 205 | ||
1595 | 205 | >>> print first_tag_by_class(nopriv_browser.contents, 'warning message') | 206 | >>> print first_tag_by_class(nopriv_browser.contents, 'warning message') |
1596 | 206 | None | 207 | None |
1597 | 207 | 208 | ||
1598 | === modified file 'lib/lp/soyuz/templates/archive-index.pt' | |||
1599 | --- lib/lp/soyuz/templates/archive-index.pt 2010-11-10 15:33:47 +0000 | |||
1600 | +++ lib/lp/soyuz/templates/archive-index.pt 2011-01-30 20:24:55 +0000 | |||
1601 | @@ -31,9 +31,7 @@ | |||
1602 | 31 | This archive has been disabled. | 31 | This archive has been disabled. |
1603 | 32 | </p> | 32 | </p> |
1604 | 33 | 33 | ||
1608 | 34 | <div id="edit-description" class="lazr-multiline-edit" | 34 | <tal:widget replace="structure view/archive_description_html"/> |
1606 | 35 | tal:content="structure view/archive_description_html" | ||
1607 | 36 | /> | ||
1609 | 37 | </div> | 35 | </div> |
1610 | 38 | 36 | ||
1611 | 39 | <tal:ppa-upload-hint condition="context/is_ppa"> | 37 | <tal:ppa-upload-hint condition="context/is_ppa"> |
1612 | 40 | 38 | ||
1613 | === modified file 'lib/lp/soyuz/windmill/tests/test_ppainlineedit.py' | |||
1614 | --- lib/lp/soyuz/windmill/tests/test_ppainlineedit.py 2010-10-18 12:56:47 +0000 | |||
1615 | +++ lib/lp/soyuz/windmill/tests/test_ppainlineedit.py 2011-01-30 20:24:55 +0000 | |||
1616 | @@ -20,7 +20,7 @@ | |||
1617 | 20 | 20 | ||
1618 | 21 | ppa_displayname_inline_edit_test = widgets.InlineEditorWidgetTest( | 21 | ppa_displayname_inline_edit_test = widgets.InlineEditorWidgetTest( |
1619 | 22 | url='%s/~cprov/+archive/ppa' % SoyuzWindmillLayer.base_url, | 22 | url='%s/~cprov/+archive/ppa' % SoyuzWindmillLayer.base_url, |
1621 | 23 | widget_id='displayname', | 23 | widget_id='edit-displayname', |
1622 | 24 | expected_value='PPA for Celso Providelo', | 24 | expected_value='PPA for Celso Providelo', |
1623 | 25 | new_value="Celso's default PPA", | 25 | new_value="Celso's default PPA", |
1624 | 26 | name='test_ppa_displayname_inline_edit', | 26 | name='test_ppa_displayname_inline_edit', |
1625 | 27 | 27 | ||
1626 | === modified file 'lib/lp/translations/browser/hastranslationimports.py' | |||
1627 | --- lib/lp/translations/browser/hastranslationimports.py 2010-11-23 23:22:27 +0000 | |||
1628 | +++ lib/lp/translations/browser/hastranslationimports.py 2011-01-30 20:24:55 +0000 | |||
1629 | @@ -29,13 +29,13 @@ | |||
1630 | 29 | from canonical.launchpad.webapp.authorization import check_permission | 29 | from canonical.launchpad.webapp.authorization import check_permission |
1631 | 30 | from canonical.launchpad.webapp.batching import TableBatchNavigator | 30 | from canonical.launchpad.webapp.batching import TableBatchNavigator |
1632 | 31 | from canonical.launchpad.webapp.vocabulary import ForgivingSimpleVocabulary | 31 | from canonical.launchpad.webapp.vocabulary import ForgivingSimpleVocabulary |
1633 | 32 | from canonical.widgets.lazrjs import vocabulary_to_choice_edit_items | ||
1634 | 33 | from lp.app.browser.launchpadform import ( | 32 | from lp.app.browser.launchpadform import ( |
1635 | 34 | action, | 33 | action, |
1636 | 35 | custom_widget, | 34 | custom_widget, |
1637 | 36 | LaunchpadFormView, | 35 | LaunchpadFormView, |
1638 | 37 | safe_action, | 36 | safe_action, |
1639 | 38 | ) | 37 | ) |
1640 | 38 | from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items | ||
1641 | 39 | from lp.app.errors import UnexpectedFormData | 39 | from lp.app.errors import UnexpectedFormData |
1642 | 40 | from lp.registry.interfaces.distribution import IDistribution | 40 | from lp.registry.interfaces.distribution import IDistribution |
1643 | 41 | from lp.registry.interfaces.pillar import IPillarNameSet | 41 | from lp.registry.interfaces.pillar import IPillarNameSet |
Hi Tim,
This is a very nice consolidation of the widget infrastructure! I have a
couple of questions and suggestion for enhancements.
Cheers
> === modified file 'lib/canonical/ widgets/ lazrjs. py' browser_ request( )
> +class WidgetBase:
> + """Useful methods for all widgets."""
>
> + def __init__(self, context, exported_field, content_box_id):
> + self.context = context
> + self.exported_field = exported_field
> +
> + self.request = get_current_
That makes us depend on global state. To unit test widget python code, it will
require setting the interaction.
Maybe it's not that much of a problem...
> + self.attribute_name = exported_ field._ _name__
> +
> + if content_box_id is None:
> + content_box_id = "edit-%s" % self.attribute_name
We need to cater for the use-case when we'll have multiple edit widget on the
page, so I'd suggest putting in a counter there. (That's why we were using
_generate_id() before)
> + self.content_box_id = content_box_id method_ name = None field.queryTagg edValue( LAZR_WEBSERVICE _EXPORTED) get('mutator_ annotations' ) method_ name = mutator_ method. __name_ _
> +
> + self.mutator_
> + ws_stack = exported_
> + if ws_stack is None:
> + # The field may be a copy, or similarly named to one we care
> + # about.
> + self.api_attribute = self.attribute_name
> + else:
> + self.api_attribute = ws_stack['as']
> + mutator_info = ws_stack.
> + if mutator_info is not None:
> + mutator_method, mutator_extra = mutator_info
> + self.mutator_
Why do we need to care about the mutator method name? It should be
transparent: you simply PATCH the field to the new value and lazr.restful will
call the mutator appropriately automatically. The 'as' stuff is required
though.
> + url(self. context, force_local_ path=True)
> + @property
> + def resource_uri(self):
> + return canonical_
> +
Why force_local_path? (Subtle hint to add a comment there.)
> + @property uri(self) : dumps(self. resource_ uri) self.context, self.attribute_ name): method_ name is not None: ignee. self.context, self.mutator_ method_ name)
> + def json_resource_
> + return simplejson.
> +
> + @classmethod
> + def _generate_id(cls):
> + """Return a presumably unique id for this widget."""
> + cls.last_id += 1
> + return '%s-id-%d' % (cls.widget_type, cls.last_id)
> +
> + @property
> + def can_write(self):
> + if canWrite(
> + return True
> + elif self.mutator_
> + # The user may not have write access on the attribute itself, but
> + # the REST API may have a mutator method configured, such as
> + # transitionToAss
> + return canAccess(
> + else:
> + return False
Hmm, I see why you need to look at the mutator then. Might want to add a
comment to explain it.
> + WidgetBase) : tBase, self)....
> +
> +class TextWidgetBase(
> +
> + def __init__(self, context, exported_field, content_box_id,
> + accept_empty, title, edit_view, edit_url):
> + super(TextWidge