Merge lp:~thumper/launchpad/refactor-lazrjs-text-widgets into lp:launchpad

Proposed by Tim Penhey
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
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Approve
Review via email: mp+47634@code.launchpad.net

Commit message

[r=flacoste][ui=none][bug=708267,708980] Refactor the text lazrjs widgets.

Description of the change

This branch started out as a quick refactoring of the TextLineEditorWidget and the TextAreaEditorWidget.

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.

To post a comment you must log in.
Revision history for this message
Francis J. Lacoste (flacoste) wrote :
Download full text (6.3 KiB)

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'
> +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_browser_request()

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
> +
> + self.mutator_method_name = None
> + ws_stack = exported_field.queryTaggedValue(LAZR_WEBSERVICE_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.get('mutator_annotations')
> + if mutator_info is not None:
> + mutator_method, mutator_extra = mutator_info
> + self.mutator_method_name = mutator_method.__name__

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.

> +
> + @property
> + def resource_uri(self):
> + return canonical_url(self.context, force_local_path=True)
> +

Why force_local_path? (Subtle hint to add a comment there.)

> + @property
> + def json_resource_uri(self):
> + return simplejson.dumps(self.resource_uri)
> +
> + @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(self.context, self.attribute_name):
> + return True
> + elif self.mutator_method_name is not None:
> + # The user may not have write access on the attribute itself, but
> + # the REST API may have a mutator method configured, such as
> + # transitionToAssignee.
> + return canAccess(self.context, self.mutator_method_name)
> + else:
> + return False

Hmm, I see why you need to look at the mutator then. Might want to add a
comment to explain it.

> +
> +
> +class TextWidgetBase(WidgetBase):
> +
> + def __init__(self, context, exported_field, content_box_id,
> + accept_empty, title, edit_view, edit_url):
> + super(TextWidgetBase, self)....

Read more...

review: Needs Information
Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (3.2 KiB)

On Fri, 28 Jan 2011 05:43:28 Francis J. Lacoste wrote:
> > === modified file 'lib/canonical/widgets/lazrjs.py'
> > +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_browser_request()
>
> 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_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)

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_url(self.context, force_local_path=True)
> > +
>
> Why force_local_path? (Subtle hint to add a comment there.)

Docstring added.

> > +class TextWidgetBase(WidgetBase):
> > +
> > + def __init__(self, context, exported_field, content_box_id,
> > + accept_empty, title, edit_view, edit_url):
> > + super(TextWidgetBase, self).__init__(
> > + context, exported_field, content_box_id)
> > + if edit_url is None:
> > + edit_url = canonical_url(self.context, view_name=edit_view)
>
> 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.normalize_uri(resource_uri)
>
> 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...

Read more...

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Really nice!

Thanks for the extensive rewrite of the documentation!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== renamed file 'lib/canonical/widgets/lazrjs.py' => 'lib/lp/app/browser/lazrjs.py'
--- lib/canonical/widgets/lazrjs.py 2011-01-21 04:32:26 +0000
+++ lib/lp/app/browser/lazrjs.py 2011-01-30 20:24:55 +0000
@@ -1,19 +1,18 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Wrappers for lazr-js widgets."""4"""Wrappers for lazr-js widgets."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'InlineEditPickerWidget',
9 'standard_text_html_representation',
8 'TextAreaEditorWidget',10 'TextAreaEditorWidget',
9 'InlineEditPickerWidget',11 'TextLineEditorWidget',
10 'vocabulary_to_choice_edit_items',12 'vocabulary_to_choice_edit_items',
11 'TextLineEditorWidget',
12 ]13 ]
1314
14import cgi
15import simplejson15import simplejson
16from textwrap import dedent
1716
18from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile17from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
19from zope.component import getUtility18from zope.component import getUtility
@@ -21,255 +20,209 @@
21from zope.schema.interfaces import IVocabulary20from zope.schema.interfaces import IVocabulary
22from zope.schema.vocabulary import getVocabularyRegistry21from zope.schema.vocabulary import getVocabularyRegistry
2322
24from lazr.restful.interfaces import IWebServiceClientRequest23from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
24from canonical.lazr.utils import get_current_browser_request
25from canonical.lazr.utils import safe_hasattr25from canonical.lazr.utils import safe_hasattr
2626
27from canonical.launchpad.webapp.interfaces import ILaunchBag27from canonical.launchpad.webapp.interfaces import ILaunchBag
28from canonical.launchpad.webapp.publisher import canonical_url28from canonical.launchpad.webapp.publisher import canonical_url
29from canonical.launchpad.webapp.vocabulary import IHugeVocabulary29from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
30from lp.app.browser.stringformatter import FormattersAPI
30from lp.services.propertycache import cachedproperty31from lp.services.propertycache import cachedproperty
3132
3233
33class TextLineEditorWidget:34class WidgetBase:
35 """Useful methods for all widgets."""
36
37 def __init__(self, context, exported_field, content_box_id):
38 self.context = context
39 self.exported_field = exported_field
40
41 self.request = get_current_browser_request()
42 self.attribute_name = exported_field.__name__
43 self.optional_field = not exported_field.required
44
45 if content_box_id is None:
46 content_box_id = "edit-%s" % self.attribute_name
47 self.content_box_id = content_box_id
48
49 # The mutator method name is used to determine whether or not the
50 # current user has permission to alter the attribute if the attribute
51 # is using a mutator function.
52 self.mutator_method_name = None
53 ws_stack = exported_field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
54 if ws_stack is None:
55 # The field may be a copy, or similarly named to one we care
56 # about.
57 self.api_attribute = self.attribute_name
58 else:
59 self.api_attribute = ws_stack['as']
60 mutator_info = ws_stack.get('mutator_annotations')
61 if mutator_info is not None:
62 mutator_method, mutator_extra = mutator_info
63 self.mutator_method_name = mutator_method.__name__
64
65 @property
66 def resource_uri(self):
67 """A local path to the context object.
68
69 The javascript uses the normalize_uri method that adds the appropriate
70 prefix to the uri. Doing it this way avoids needing to adapt the
71 current request into a webservice request in order to get an api url.
72 """
73 return canonical_url(self.context, force_local_path=True)
74
75 @property
76 def json_resource_uri(self):
77 return simplejson.dumps(self.resource_uri)
78
79 @property
80 def can_write(self):
81 """Can the current user write to the attribute."""
82 if canWrite(self.context, self.attribute_name):
83 return True
84 elif self.mutator_method_name is not None:
85 # The user may not have write access on the attribute itself, but
86 # the REST API may have a mutator method configured, such as
87 # transitionToAssignee.
88 return canAccess(self.context, self.mutator_method_name)
89 else:
90 return False
91
92
93class TextWidgetBase(WidgetBase):
94 """Abstract base for the single and multiline text editor widgets."""
95
96 def __init__(self, context, exported_field, title, content_box_id,
97 edit_view, edit_url):
98 super(TextWidgetBase, self).__init__(
99 context, exported_field, content_box_id)
100 if edit_url is None:
101 edit_url = canonical_url(self.context, view_name=edit_view)
102 self.edit_url = edit_url
103 self.accept_empty = simplejson.dumps(self.optional_field)
104 self.title = title
105 self.json_attribute = simplejson.dumps(self.api_attribute)
106 self.widget_css_selector = simplejson.dumps('#' + self.content_box_id)
107
108 @property
109 def json_attribute_uri(self):
110 return simplejson.dumps(self.resource_uri + '/' + self.api_attribute)
111
112
113class TextLineEditorWidget(TextWidgetBase):
34 """Wrapper for the lazr-js inlineedit/editor.js widget."""114 """Wrapper for the lazr-js inlineedit/editor.js widget."""
35115
36 # Class variable used to generate a unique per-page id for the widget116 __call__ = ViewPageTemplateFile('../templates/text-line-editor.pt')
37 # in case it's not provided.117
38 last_id = 0118 def __init__(self, context, exported_field, title, tag,
39119 content_box_id=None, edit_view="+edit", edit_url=None,
40 # The HTML template used to render the widget.
41 # Replacements:
42 # activation_script: the JS script to active the widget
43 # attribute: the name of the being edited
44 # context_url: the url to the current context
45 # edit_url: the URL used to edit the value when JS is turned off
46 # id: the widget unique id
47 # title: the widget title
48 # trigger: the trigger (button) HTML code
49 # value: the current field value
50 WIDGET_TEMPLATE = dedent(u"""\
51 <%(tag)s id="%(id)s"><span
52 class="yui3-editable_text-text">%(value)s</span>
53 %(trigger)s
54 </%(tag)s>
55 %(activation_script)s
56 """)
57
58 # Template for the trigger button.
59 TRIGGER_TEMPLATE = dedent(u"""\
60 <a href="%(edit_url)s" class="yui3-editable_text-trigger sprite edit"
61 ></a>
62 """)
63
64 # Template for the activation script.
65 ACTIVATION_TEMPLATE = dedent(u"""\
66 <script>
67 LPS.use('lazr.editor', 'lp.client.plugins', function (Y) {
68 var widget = new Y.EditableText({
69 contentBox: '#%(id)s',
70 accept_empty: %(accept_empty)s,
71 width: '%(width)s',
72 initial_value_override: %(initial_value_override)s
73 });
74 widget.editor.plug({
75 fn: Y.lp.client.plugins.PATCHPlugin, cfg: {
76 patch: '%(public_attribute)s',
77 resource: '%(context_url)s'}});
78 widget.render();
79 });
80 </script>
81 """)
82
83 def __init__(self, context, attribute, edit_url, id=None, title="Edit",
84 tag='h1', public_attribute=None, accept_empty=False,
85 default_text=None, initial_value_override=None, width=None):120 default_text=None, initial_value_override=None, width=None):
86 """Create a widget wrapper.121 """Create a widget wrapper.
87122
88 :param context: The object that is being edited.123 :param context: The object that is being edited.
89 :param attribute: The name of the attribute being edited.124 :param exported_field: The attribute being edited. This should be
90 :param edit_url: The URL to use for editing when the user isn't logged125 a field from an interface of the form ISomeInterface['fieldname']
91 in and when JS is off.126 :param title: The string to use as the link title.
92 :param id: The HTML id to use for this widget. Automatically
93 generated if this is not provided.
94 :param title: The string to use as the link title. Defaults to 'Edit'.
95 :param tag: The HTML tag to use.127 :param tag: The HTML tag to use.
96 :param public_attribute: If given, the name of the attribute in the128 :param content_box_id: The HTML id to use for this widget.
97 public webservice API.129 Defaults to edit-<attribute name>.
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
131 one is not specified.
132 :param edit_url: The URL to use for editing when the user isn't logged
133 in and when JS is off. Defaults to the edit_view on the context.
99 :param default_text: Text to show in the unedited field, if the134 :param default_text: Text to show in the unedited field, if the
100 parameter value is missing or None.135 parameter value is missing or None.
101 :param initial_value_override: Use this text for the initial edited136 :param initial_value_override: Use this text for the initial edited
102 field value instead of the attribute's current value.137 field value instead of the attribute's current value.
103 :param width: Initial widget width.138 :param width: Initial widget width.
104 """139 """
105 self.context = context140 super(TextLineEditorWidget, self).__init__(
106 self.attribute = attribute141 context, exported_field, title, content_box_id,
107 self.edit_url = edit_url142 edit_view, edit_url)
108 self.tag = tag143 self.tag = tag
109 if accept_empty:
110 self.accept_empty = 'true'
111 else:
112 self.accept_empty = 'false'
113 if public_attribute is None:
114 self.public_attribute = attribute
115 else:
116 self.public_attribute = public_attribute
117 if id is None:
118 self.id = self._generate_id()
119 else:
120 self.id = id
121 self.title = title
122 self.default_text = default_text144 self.default_text = default_text
123 self.initial_value_override = initial_value_override145 self.initial_value_override = simplejson.dumps(initial_value_override)
124 self.width = width146 self.width = simplejson.dumps(width)
125147
126 @classmethod148 @property
127 def _generate_id(cls):149 def open_tag(self):
128 """Return a presumably unique id for this widget."""150 return '<%s id="%s">' % (self.tag, self.content_box_id)
129 cls.last_id += 1151
130 return 'inline-textline-editor-id%d' % cls.last_id152 @property
131153 def close_tag(self):
132 def __call__(self):154 return '</%s>' % self.tag
133 """Return the HTML to include to render the widget."""155
134 # We can't use the value None because of the cgi.escape() and because156 @property
135 # that wouldn't look very good in the ui!157 def value(self):
136 value = getattr(self.context, self.attribute, self.default_text)158 text = getattr(self.context, self.attribute_name, self.default_text)
137 if value is None:159 if text is None:
138 value = self.default_text160 text = self.default_text
139 params = {161 return text
140 'activation_script': '',162
141 'trigger': '',163
142 'edit_url': self.edit_url,164class TextAreaEditorWidget(TextWidgetBase):
143 'id': self.id,
144 'title': self.title,
145 'value': cgi.escape(value),
146 'context_url': canonical_url(
147 self.context, path_only_if_possible=True),
148 'attribute': self.attribute,
149 'tag': self.tag,
150 'public_attribute': self.public_attribute,
151 'accept_empty': self.accept_empty,
152 'initial_value_override': simplejson.dumps(
153 self.initial_value_override),
154 'width': self.width,
155 }
156 # Only display the trigger link and the activation script if
157 # the user can write the attribute.
158 if canWrite(self.context, self.attribute):
159 params['trigger'] = self.TRIGGER_TEMPLATE % params
160 params['activation_script'] = self.ACTIVATION_TEMPLATE % params
161 return self.WIDGET_TEMPLATE % params
162
163
164class TextAreaEditorWidget(TextLineEditorWidget):
165 """Wrapper for the multine-line lazr-js inlineedit/editor.js widget."""165 """Wrapper for the multine-line lazr-js inlineedit/editor.js widget."""
166166
167 def __init__(self, *args, **kwds):167 __call__ = ViewPageTemplateFile('../templates/text-area-editor.pt')
168 """Create the widget wrapper."""168
169 if 'value' in kwds:169 def __init__(self, context, exported_field, title, content_box_id=None,
170 self.value = kwds.get('value', '')170 edit_view="+edit", edit_url=None,
171 kwds.pop('value')171 hide_empty=True, linkify_text=True):
172 super(TextAreaEditorWidget, self).__init__(*args, **kwds)172 """Create the widget wrapper.
173173
174 # The HTML template used to render the widget.174 :param context: The object that is being edited.
175 # Replacements:175 :param exported_field: The attribute being edited. This should be
176 # activation_script: the JS script to active the widget176 a field from an interface of the form ISomeInterface['fieldname']
177 # attribute: the name of the being edited177 :param title: The string to use as the link title.
178 # context_url: the url to the current context178 :param content_box_id: The HTML id to use for this widget.
179 # edit_url: the URL used to edit the value when JS is turned off179 Defaults to edit-<attribute name>.
180 # id: the widget unique id180 :param edit_view: The view name to use to generate the edit_url if
181 # title: the widget title181 one is not specified.
182 # trigger: the trigger (button) HTML code182 :param edit_url: The URL to use for editing when the user isn't logged
183 # value: the current field value183 in and when JS is off. Defaults to the edit_view on the context.
184 WIDGET_TEMPLATE = dedent(u"""\184 :param hide_empty: If the attribute has no value, or is empty, then
185 <div id="multi-text-editor">185 hide the editor by adding the "unseen" CSS class.
186 <div class="clearfix">186 :param linkify_text: If True the HTML version of the text will have
187 %(edit_controls)s187 things that look like links made into anchors.
188 <h2>%(title)s</h2>188 """
189 </div>189 super(TextAreaEditorWidget, self).__init__(
190 <div class="yui3-editable_text-text">%(value)s</div>190 context, exported_field, title, content_box_id,
191 </div>191 edit_view, edit_url)
192 %(activation_script)s192 self.hide_empty = hide_empty
193 """)193 self.linkify_text = linkify_text
194194
195 CONTROLS_TEMPLATE = dedent(u"""\195 @property
196 <div class="edit-controls">196 def tag_class(self):
197 &nbsp;197 """The CSS class for the widget."""
198 %(trigger)s198 classes = ['lazr-multiline-edit']
199 </div>199 if self.hide_empty and not self.value:
200 """)200 classes.append('unseen')
201201 return ' '.join(classes)
202 ACTIVATION_TEMPLATE = dedent(u"""\202
203 <script>203 @cachedproperty
204 LPS.use('lazr.editor', 'lp.client.plugins', function (Y) {204 def value(self):
205 var widget = new Y.EditableText({205 text = getattr(self.context, self.attribute_name, None)
206 contentBox: '#%(id)s',206 return standard_text_html_representation(text, self.linkify_text)
207 accept_empty: %(accept_empty)s,207
208 multiline: true,208
209 buttons: 'top'209class InlineEditPickerWidget(WidgetBase):
210 });
211 widget.editor.plug({
212 fn: Y.lp.client.plugins.PATCHPlugin, cfg: {
213 patch: '%(attribute)s',
214 resource: '%(context_url)s/%(attribute)s',
215 patch_field: true,
216 accept: 'application/xhtml+xml'
217 }});
218 if (!Y.UA.opera) {
219 widget.render();
220 }
221 var lpns = Y.namespace('lp');
222 if (!lpns.widgets) {
223 lpns.widgets = {};
224 }
225 lpns.widgets['%(id)s'] = widget;
226 });
227 </script>
228 """)
229
230 def __call__(self):
231 """Return the HTML to include to render the widget."""
232 params = {
233 'activation_script': '',
234 'trigger': '',
235 'edit_url': self.edit_url,
236 'id': self.id,
237 'title': self.title,
238 'value': self.value,
239 'context_url': canonical_url(
240 self.context, path_only_if_possible=True),
241 'attribute': self.attribute,
242 'accept_empty': self.accept_empty,
243 'edit_controls': '',
244 }
245 # Only display the trigger link and the activation script if
246 # the user can write the attribute.
247 if canWrite(self.context, self.attribute):
248 params['trigger'] = self.TRIGGER_TEMPLATE % params
249 params['activation_script'] = self.ACTIVATION_TEMPLATE % params
250 params['edit_controls'] = self.CONTROLS_TEMPLATE % params
251 return self.WIDGET_TEMPLATE % params
252
253
254class InlineEditPickerWidget:
255 """Wrapper for the lazr-js picker widget.210 """Wrapper for the lazr-js picker widget.
256211
257 This widget is not for editing form values like the212 This widget is not for editing form values like the
258 VocabularyPickerWidget.213 VocabularyPickerWidget.
259 """214 """
260215
261 last_id = 0216 __call__ = ViewPageTemplateFile('../templates/inline-picker.pt')
262 __call__ = ViewPageTemplateFile('templates/inline-picker.pt')
263217
264 def __init__(self, context, request, interface_attribute, default_html,218 def __init__(self, context, exported_field, default_html,
265 content_box_id=None, header='Select an item',219 content_box_id=None, header='Select an item',
266 step_title='Search', remove_button_text='Remove',220 step_title='Search', remove_button_text='Remove',
267 null_display_value='None'):221 null_display_value='None'):
268 """Create a widget wrapper.222 """Create a widget wrapper.
269223
270 :param context: The object that is being edited.224 :param context: The object that is being edited.
271 :param request: The request object.225 :param exported_field: The attribute being edited. This should be
272 :param interface_attribute: The attribute being edited. This should be
273 a field from an interface of the form ISomeInterface['fieldname']226 a field from an interface of the form ISomeInterface['fieldname']
274 :param default_html: Default display of attribute.227 :param default_html: Default display of attribute.
275 :param content_box_id: The HTML id to use for this widget. Automatically228 :param content_box_id: The HTML id to use for this widget. Automatically
@@ -279,17 +232,9 @@
279 :param remove_button_text: Override default button text: "Remove"232 :param remove_button_text: Override default button text: "Remove"
280 :param null_display_value: This will be shown for a missing value233 :param null_display_value: This will be shown for a missing value
281 """234 """
282 self.context = context235 super(InlineEditPickerWidget, self).__init__(
283 self.request = request236 context, exported_field, content_box_id)
284 self.default_html = default_html237 self.default_html = default_html
285 self.interface_attribute = interface_attribute
286 self.attribute_name = interface_attribute.__name__
287
288 if content_box_id is None:
289 self.content_box_id = self._generate_id()
290 else:
291 self.content_box_id = content_box_id
292
293 self.header = header238 self.header = header
294 self.step_title = step_title239 self.step_title = step_title
295 self.remove_button_text = remove_button_text240 self.remove_button_text = remove_button_text
@@ -297,26 +242,29 @@
297242
298 # JSON encoded attributes.243 # JSON encoded attributes.
299 self.json_content_box_id = simplejson.dumps(self.content_box_id)244 self.json_content_box_id = simplejson.dumps(self.content_box_id)
300 self.json_attribute = simplejson.dumps(self.attribute_name + '_link')245 self.json_attribute = simplejson.dumps(self.api_attribute + '_link')
301 self.json_vocabulary_name = simplejson.dumps(246 self.json_vocabulary_name = simplejson.dumps(
302 self.interface_attribute.vocabularyName)247 self.exported_field.vocabularyName)
303 self.show_remove_button = not self.interface_attribute.required
304248
305 @property249 @property
306 def config(self):250 def config(self):
307 return simplejson.dumps(251 return dict(
308 dict(header=self.header, step_title=self.step_title,252 header=self.header, step_title=self.step_title,
309 remove_button_text=self.remove_button_text,253 remove_button_text=self.remove_button_text,
310 null_display_value=self.null_display_value,254 null_display_value=self.null_display_value,
311 show_remove_button=self.show_remove_button,255 show_remove_button=self.optional_field,
312 show_assign_me_button=self.show_assign_me_button,256 show_assign_me_button=self.show_assign_me_button,
313 show_search_box=self.show_search_box))257 show_search_box=self.show_search_box)
258
259 @property
260 def json_config(self):
261 return simplejson.dumps(self.config)
314262
315 @cachedproperty263 @cachedproperty
316 def vocabulary(self):264 def vocabulary(self):
317 registry = getVocabularyRegistry()265 registry = getVocabularyRegistry()
318 return registry.get(266 return registry.get(
319 IVocabulary, self.interface_attribute.vocabularyName)267 IVocabulary, self.exported_field.vocabularyName)
320268
321 @property269 @property
322 def show_search_box(self):270 def show_search_box(self):
@@ -329,43 +277,6 @@
329 user = getUtility(ILaunchBag).user277 user = getUtility(ILaunchBag).user
330 return user and user in vocabulary278 return user and user in vocabulary
331279
332 @classmethod
333 def _generate_id(cls):
334 """Return a presumably unique id for this widget."""
335 cls.last_id += 1
336 return 'inline-picker-activator-id-%d' % cls.last_id
337
338 @property
339 def json_resource_uri(self):
340 return simplejson.dumps(
341 canonical_url(
342 self.context, request=IWebServiceClientRequest(self.request),
343 path_only_if_possible=True))
344
345 @property
346 def can_write(self):
347 if canWrite(self.context, self.attribute_name):
348 return True
349 else:
350 # The user may not have write access on the attribute itself, but
351 # the REST API may have a mutator method configured, such as
352 # transitionToAssignee.
353 #
354 # We look at the top of the annotation stack, since Ajax
355 # requests always go to the most recent version of the web
356 # service.
357 try:
358 exported_tag_stack = self.interface_attribute.getTaggedValue(
359 'lazr.restful.exported')
360 except KeyError:
361 return False
362 mutator_info = exported_tag_stack.get('mutator_annotations')
363 if mutator_info is not None:
364 mutator_method, mutator_extra = mutator_info
365 return canAccess(self.context, mutator_method.__name__)
366 else:
367 return False
368
369280
370def vocabulary_to_choice_edit_items(281def vocabulary_to_choice_edit_items(
371 vocab, css_class_prefix=None, disabled_items=None, as_json=False,282 vocab, css_class_prefix=None, disabled_items=None, as_json=False,
@@ -412,3 +323,13 @@
412 else:323 else:
413 return items324 return items
414325
326
327def standard_text_html_representation(value, linkify_text=True):
328 """Render a string for html display.
329
330 For this we obfuscate email and render as html.
331 """
332 if value is None:
333 return ''
334 nomail = FormattersAPI(value).obfuscate_email()
335 return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text)
415336
=== renamed file 'lib/canonical/widgets/tests/test_inlineeditpickerwidget.py' => 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py'
--- lib/canonical/widgets/tests/test_inlineeditpickerwidget.py 2011-01-20 21:16:36 +0000
+++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2011-01-30 20:24:55 +0000
@@ -9,7 +9,7 @@
9from zope.schema import Choice9from zope.schema import Choice
1010
11from canonical.testing.layers import DatabaseFunctionalLayer11from canonical.testing.layers import DatabaseFunctionalLayer
12from canonical.widgets.lazrjs import InlineEditPickerWidget12from lp.app.browser.lazrjs import InlineEditPickerWidget
13from lp.testing import (13from lp.testing import (
14 login_person,14 login_person,
15 TestCaseWithFactory,15 TestCaseWithFactory,
@@ -23,35 +23,35 @@
23 def getWidget(self, **kwargs):23 def getWidget(self, **kwargs):
24 class ITest(Interface):24 class ITest(Interface):
25 test_field = Choice(**kwargs)25 test_field = Choice(**kwargs)
26 return InlineEditPickerWidget(None, None, ITest['test_field'], None)26 return InlineEditPickerWidget(None, ITest['test_field'], None)
2727
28 def test_huge_vocabulary_is_searchable(self):28 def test_huge_vocabulary_is_searchable(self):
29 # Make sure that when given a field for a huge vocabulary, the picker29 # Make sure that when given a field for a huge vocabulary, the picker
30 # is set to show the search box.30 # is set to show the search box.
31 widget = self.getWidget(vocabulary='ValidPersonOrTeam')31 widget = self.getWidget(vocabulary='ValidPersonOrTeam')
32 self.assertTrue(widget.show_search_box)32 self.assertTrue(widget.config['show_search_box'])
3333
34 def test_normal_vocabulary_is_not_searchable(self):34 def test_normal_vocabulary_is_not_searchable(self):
35 # Make sure that when given a field for a normal vocabulary, the picker35 # Make sure that when given a field for a normal vocabulary, the picker
36 # is set to show the search box.36 # is set to show the search box.
37 widget = self.getWidget(vocabulary='UserTeamsParticipation')37 widget = self.getWidget(vocabulary='UserTeamsParticipation')
38 self.assertFalse(widget.show_search_box)38 self.assertFalse(widget.config['show_search_box'])
3939
40 def test_required_fields_dont_have_a_remove_link(self):40 def test_required_fields_dont_have_a_remove_link(self):
41 widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)41 widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
42 self.assertFalse(widget.show_remove_button)42 self.assertFalse(widget.config['show_remove_button'])
4343
44 def test_optional_fields_do_have_a_remove_link(self):44 def test_optional_fields_do_have_a_remove_link(self):
45 widget = self.getWidget(45 widget = self.getWidget(
46 vocabulary='ValidPersonOrTeam', required=False)46 vocabulary='ValidPersonOrTeam', required=False)
47 self.assertTrue(widget.show_remove_button)47 self.assertTrue(widget.config['show_remove_button'])
4848
49 def test_assign_me_exists_if_user_in_vocabulary(self):49 def test_assign_me_exists_if_user_in_vocabulary(self):
50 widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)50 widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
51 login_person(self.factory.makePerson())51 login_person(self.factory.makePerson())
52 self.assertTrue(widget.show_assign_me_button)52 self.assertTrue(widget.config['show_assign_me_button'])
5353
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):
55 widget = self.getWidget(vocabulary='TargetPPAs', required=True)55 widget = self.getWidget(vocabulary='TargetPPAs', required=True)
56 login_person(self.factory.makePerson())56 login_person(self.factory.makePerson())
57 self.assertFalse(widget.show_assign_me_button)57 self.assertFalse(widget.config['show_assign_me_button'])
5858
=== modified file 'lib/lp/app/browser/webservice.py'
--- lib/lp/app/browser/webservice.py 2011-01-20 22:27:42 +0000
+++ lib/lp/app/browser/webservice.py 2011-01-30 20:24:55 +0000
@@ -18,7 +18,7 @@
18 )18 )
19from zope.schema.interfaces import IText19from zope.schema.interfaces import IText
2020
21from lp.app.browser.stringformatter import FormattersAPI21from lp.app.browser.lazrjs import standard_text_html_representation
22from lp.app.browser.tales import format_link22from lp.app.browser.tales import format_link
2323
2424
@@ -44,12 +44,4 @@
44@implementer(IFieldHTMLRenderer)44@implementer(IFieldHTMLRenderer)
45def text_xhtml_representation(context, field, request):45def text_xhtml_representation(context, field, request):
46 """Render text as XHTML using the webservice."""46 """Render text as XHTML using the webservice."""
47 formatter = FormattersAPI47 return standard_text_html_representation
48
49 def renderer(value):
50 if value is None:
51 return ''
52 nomail = formatter(value).obfuscate_email()
53 return formatter(nomail).text_to_html()
54
55 return renderer
5648
=== renamed file 'lib/canonical/launchpad/doc/lazr-js-widgets.txt' => 'lib/lp/app/doc/lazr-js-widgets.txt'
--- lib/canonical/launchpad/doc/lazr-js-widgets.txt 2011-01-21 04:36:36 +0000
+++ lib/lp/app/doc/lazr-js-widgets.txt 2011-01-30 20:24:55 +0000
@@ -1,31 +1,40 @@
1LAZR JS Wrappers1LAZR JS Wrappers
2================2================
33
4The canonical.widgets.lazrjs module contains a bunch of wrapper4The lp.app.browser.lazrjs module contains several classes that simplify the
5for widgets defined in Lazr-JS.5use of widgets defined in Lazr-JS.
6
7When rendering these widgets in page templates, all you need to do is 'call'
8them. TAL will do this for you.
9
10 <tal:widget replace="structure view/nifty_widget"/>
11
612
7TextLineEditorWidget13TextLineEditorWidget
8--------------------------14--------------------
915
10We have a convenient wrapper for the inlineedit/editor JS widget in16We have a convenient wrapper for the inlineedit/editor JS widget in
11TextLineEditorWidget.17TextLineEditorWidget.
1218
13 >>> from canonical.launchpad.webapp.publisher import canonical_url19 >>> from lp.app.browser.lazrjs import TextLineEditorWidget
14 >>> from canonical.widgets.lazrjs import TextLineEditorWidget20
15 >>> bug = factory.makeBug(title='My bug is > important')21The bare minimum that you need to provide the widget is the object that you
1622are editing, and the exported field that is being edited, and a title for the
17The wrapper takes as arguments the object and the attribute of the23edit link that is rendered as the itle of the anchor so it shows on mouse
18object that is being edited, as well as the URL to use for editing when24over, and the tag that surrounds the text.
19when JS is turned off.25
2026 >>> from lp.registry.interfaces.product import IProduct
21 >>> widget = TextLineEditorWidget(27 >>> product = factory.makeProduct(
22 ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'))28 ... name='widget', title='Widgets > important')
29 >>> title_field = IProduct['title']
30 >>> title = 'Edit the title'
31 >>> widget = TextLineEditorWidget(product, title_field, title, 'h1')
2332
24The widget is rendered by executing it, it prints out the attribute33The widget is rendered by executing it, it prints out the attribute
25content.34content.
2635
27 >>> print widget()36 >>> print widget()
28 <h1 id="..."><span class="yui3-editable_text-text">My bug is &gt;37 <h1 id="edit-title"><span class="yui3-editable_text-text">Widgets &gt;
29 important</span>38 important</span>
30 </h1>39 </h1>
3140
@@ -33,97 +42,229 @@
33the edit view that appears as well as a <script> tag that will change that42the edit view that appears as well as a <script> tag that will change that
34link into an AJAX control when JS is available:43link into an AJAX control when JS is available:
3544
36 >>> login('no-priv@canonical.com')45 >>> login_person(product.owner)
37 >>> print widget()46 >>> print widget()
38 <h1 id="..."><span class="yui3-editable_text-text">My bug is &gt;47 <h1 id="edit-title"><span class="yui3-editable_text-text">Widgets &gt;
39 important</span>48 important</span>
40 <a href="http://bugs.launchpad.dev/.../+edit"49 <a class="yui3-editable_text-trigger sprite edit"
41 class="yui3-editable_text-trigger sprite edit"50 href="http://launchpad.dev/widget/+edit"></a>
42 ></a>
43 </h1>51 </h1>
44 <script>52 <script>
45 ...53 ...
46 </script>54 </script>
4755
48The id and title attribute to use can be passed via parameters to the56
49constructor:57Changing the tag
5058****************
51 >>> widget = TextLineEditorWidget(59
52 ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'),60The id of the surrounding tag defaults to "edit-" followed by the name of the
53 ... id="bug-title", title="Edit this summary")61attribute being edited. This can be overridden if needed using the
54 >>> print widget()62"content_box_id" constructor argument.
55 <h1 id="bug-title">...63
56 ...class="yui3-editable_text-trigger sprite edit"...64 >>> span_widget = TextLineEditorWidget(
5765 ... product, title_field, title, 'span', content_box_id="overridden")
58The initial_value_override parameter is passed as a JSON-serialized value.66 >>> login(ANONYMOUS) # To not get the script tag rendered
5967 >>> print span_widget()
60 >>> widget = TextLineEditorWidget(68 <span id="overridden">...</span>
61 ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'),69
62 ... id="bug-title", title="Edit this summary",70
63 ... initial_value_override='This bug has no title')71Changing the edit link
64 >>> print widget()72**********************
65 <h1 id="bug-title">...73
66 ...initial_value_override: "This bug has no title"...74When there is a logged in user that has edit rights on the field being edited,
6775the edit button is shown, and has a link that takes to the user to a normal
68When the value isn't supplied it defaults to None, which is serialized as a76edit page if javascript is disabled. This link defaults to the '+edit' view
69Javascript null.77of the object being edited. This can be overridden in two ways:
7078 * change the 'edit_view' parameter to be a different view
71 >>> widget = TextLineEditorWidget(79 * provide an 'edit_url' to use instead
72 ... bug, 'title', canonical_url(bug.bugtasks[0], view_name='+edit'),80
73 ... id="bug-title", title="Edit this summary")81 >>> print widget.edit_url
74 >>> print widget()82 http://launchpad.dev/widget/+edit
75 <h1 id="bug-title">...83
76 ...initial_value_override: null...84 >>> diff_view = TextLineEditorWidget(
85 ... product, title_field, title, 'h1', edit_view='+edit-people')
86 >>> print diff_view.edit_url
87 http://launchpad.dev/widget/+edit-people
88
89 >>> diff_url = TextLineEditorWidget(
90 ... product, title_field, title, 'h1', edit_url='http://example.com/')
91 >>> print diff_url.edit_url
92 http://example.com/
93
94
95Other nifty bits
96****************
97
98You are also able to set the default text to show if the attribute has no
99value using the 'default_text' parameter. The 'initial_value_override' is
100used by the javascript widget to provide that text instead of the objects
101value (of the default_text). The width of the field can also be specified
102using the 'width' parameter (please use 'em's).
103
104For an example of these parameters, see the editor for a products programming
105languages.
106
107
108TextAreaEditorWidget
109--------------------
110
111This widget renders a multi-line editor. Example uses of this widget are:
112 * editing a bug's description
113 * editing a merge proposal's commit message or description
114 * editing a PPA's description
115
116 >>> from lp.app.browser.lazrjs import TextAreaEditorWidget
117
118The bare minimum that you need to provide the widget is the object that you
119are editing, and the exported field that is being edited, and a title for the
120edit link that is rendered as the itle of the anchor so it shows on mouse
121over.
122
123 >>> eric = factory.makePerson(name='eric')
124 >>> archive = factory.makeArchive(
125 ... owner=eric, name='ppa', description='short description')
126 >>> from lp.soyuz.interfaces.archive import IArchive
127 >>> description = IArchive['description']
128 >>> widget = TextAreaEditorWidget(archive, description, 'A title')
129
130With no-one logged in, there are no edit buttons.
131
132 >>> print widget()
133 <div id="edit-description" class="lazr-multiline-edit">
134 <div class="clearfix">
135 <h2>A title</h2>
136 </div>
137 <div class="yui3-editable_text-text"><p>short description</p></div>
138 </div>
139
140The initial text defaults to the value of the attribute, which is then passed
141through two string formatter methods to obfuscate the email and then return
142the text as HTML.
143
144When the logged in user has edit permission, the edit button is shown, and
145javascript is written to the page to hook up the links to show the multiline
146editor.
147
148 >>> login_person(eric)
149 >>> print widget()
150 <div id="edit-description" class="lazr-multiline-edit">
151 <div class="clearfix">
152 <div class="edit-controls">
153 &nbsp;
154 <a class="yui3-editable_text-trigger sprite edit"
155 href="http://launchpad.dev/~eric/+archive/ppa/+edit"></a>
156 </div>
157 <h2>A title</h2>
158 </div>
159 <div class="yui3-editable_text-text"><p>short description</p></div>
160 <script>...</script>
161 </div>
162
163
164Changing the edit link
165**********************
166
167The edit link can be changed in exactly the same way as for the
168TextLineEditorWidget above.
169
170
171Hiding the widget for empty fields
172**********************************
173
174Sometimes you don't want to show the widget if there is no content. An
175example of this can be found in the branch merge proposal view for editing the
176description or the commit message. This uses links when there is no content.
177Ideally the interaction with the links would be encoded as part of the widget
178itself, but that is an exercise left for another yak shaver.
179
180Hiding the widget is done by appending the "unseen" CSS class to the outer
181tag.
182
183 >>> archive.description = None
184 >>> from lp.services.propertycache import clear_property_cache
185 >>> clear_property_cache(widget)
186 >>> print widget()
187 <div id="edit-description" class="lazr-multiline-edit unseen">
188 ...
189
190This behaviour can be overridden by setting the "hide_empty" parameter to
191False.
192
193 >>> widget = TextAreaEditorWidget(
194 ... archive, description, 'A title', hide_empty=False)
195 >>> print widget()
196 <div id="edit-description" class="lazr-multiline-edit">
197 ...
198
199
200Not linkifying the text
201***********************
202
203A part of the standard HTML rendering is to "linkify" links. That is, turn
204words that look like hyperlinks into anchors. This is not always considered a
205good idea as some spammers can create PPAs and link to other sites in the
206descriptions. since the barrier to create a PPA is relatively low, we
207restrict the linkability of some fields. The constructor provides a
208"linkify_text" parameter that defaults to True. Set this to False to avoid
209the linkification of text. See the IArchive['description'] editor for an example.
77210
78211
79InlineEditPickerWidget212InlineEditPickerWidget
80----------------------213----------------------
81214
82The InlineEditPickerWidget can be used for any interface attribute that215The InlineEditPickerWidget provides a simple way to create a popup selector
83has a vocabulary defined for it.216widget to choose items from a vocabulary.
84217
85 >>> from zope.component import getUtility218 >>> from lp.app.browser.lazrjs import InlineEditPickerWidget
86 >>> from canonical.widgets.lazrjs import InlineEditPickerWidget219
87 >>> from lp.bugs.interfaces.bugtask import IBugTask, IBugTaskSet220The bare minimum that you need to provide the widget is the object that you
88 >>> bugtask = getUtility(IBugTaskSet).get(2)221are editing, and the exported field that is being edited, and the default
89 >>> def create_inline_edit_picker_widget():222HTML representation of the field you are editing.
90 ... view = create_initialized_view(bugtask, '+index')223
91 ... return InlineEditPickerWidget(224Since most of the things that are being chosen are entities in Launchpad, and
92 ... context=view.context,225most of those entities have URLs, a common approach is to have the default
93 ... request=view.request,226HTML be a link to that entity. There is a utility function called format_link
94 ... interface_attribute=IBugTask['assignee'],227that does the equivalent of the TALES expression 'obj/fmt:link'.
95 ... default_html='default-html',228
96 ... header='Change assignee',229 >>> from lp.app.browser.tales import format_link
97 ... step_title='Search for people or teams',230 >>> default_text = format_link(archive.owner)
98 ... remove_button_text='Remove Assignee',231
99 ... null_display_value='Nobody')232The vocabulary is determined from the field passed in. If the vocabulary is a
100233huge vocabulary (one that provides a search), then the picker is shown with an
101An unauthenticated user cannot see the activator's edit button, which234entry field to allow the user to search for an item. If the vocabulary is not
102is revealed by Y.lp.app.picker.addPickerPatcher.235huge, the different items are shown in the normal paginated way for the user
103236to select.
104 >>> login(ANONYMOUS)237
105 >>> widget = create_inline_edit_picker_widget()238 >>> owner = IArchive['owner']
106 >>> print widget.can_write239 >>> widget = InlineEditPickerWidget(archive, owner, default_text)
107 False240 >>> print widget()
108 >>> print widget()241 <span id="edit-owner">
109 <span id="inline-picker-activator-id-...">...242 <span class="yui3-activator-data-box">
110 <div class="yui3-activator-message-box yui3-activator-hidden" />243 <a...>Eric</a>
111 </span>244 </span>
112245 <button class="lazr-btn yui3-activator-act yui3-activator-hidden">
113The foo.bar user can see the activator's edit button.246 Edit
114247 </button>
115 >>> login('foo.bar@canonical.com')248 <div class="yui3-activator-message-box yui3-activator-hidden" />
116 >>> widget = create_inline_edit_picker_widget()249 </span>
117 >>> print widget.can_write250
118 True251
119 >>> print widget()252Picker headings
120 <span id="inline-picker-activator-id-...">253***************
121 ...<div class="yui3-activator-message-box yui3-activator-hidden" />254
122 </span>255The picker has two headings that are almost always desirable to customize.
123 ...Y.lp.app.picker.addPickerPatcher(...256 * "header" - Shown at the top of the picker
124257 * "step_title" - Shown just below the green progress bar
125The json_resource_uri is the canonical_url for the object in258
126WebServiceClientRequests.259To customize these, pass the named parameters into the constructor of the
127260widget.
128 >>> print widget.json_resource_uri261
129 "/firefox/+bug/1"262
263Other nifty links
264*****************
265
266If the logged in user is in the defined vocabulary (only occurs with people
267type vocabularies), a link is shown "Assign me'.
268
269If the field is optional, a "Remove" link is shown. The "Remove" text is
270customizable thought the "remove_button_text" parameter.
130271
=== modified file 'lib/lp/app/javascript/picker.js'
--- lib/lp/app/javascript/picker.js 2011-01-20 21:26:07 +0000
+++ lib/lp/app/javascript/picker.js 2011-01-30 20:24:55 +0000
@@ -39,6 +39,7 @@
39 var remove_button_text = 'Remove';39 var remove_button_text = 'Remove';
40 var null_display_value = 'None';40 var null_display_value = 'None';
41 var show_search_box = true;41 var show_search_box = true;
42 resource_uri = LP.client.normalize_uri(resource_uri)
42 var full_resource_uri = LP.client.get_absolute_uri(resource_uri);43 var full_resource_uri = LP.client.get_absolute_uri(resource_uri);
43 var current_context_uri = LP.client.cache['context']['self_link'];44 var current_context_uri = LP.client.cache['context']['self_link'];
44 var editing_main_context = (full_resource_uri == current_context_uri);45 var editing_main_context = (full_resource_uri == current_context_uri);
4546
=== renamed file 'lib/canonical/widgets/templates/inline-picker.pt' => 'lib/lp/app/templates/inline-picker.pt'
--- lib/canonical/widgets/templates/inline-picker.pt 2011-01-20 20:24:10 +0000
+++ lib/lp/app/templates/inline-picker.pt 2011-01-30 20:24:55 +0000
@@ -20,7 +20,7 @@
20 ${view/json_resource_uri},20 ${view/json_resource_uri},
21 ${view/json_attribute},21 ${view/json_attribute},
22 ${view/json_content_box_id},22 ${view/json_content_box_id},
23 ${view/config});23 ${view/json_config});
24 }, window);24 }, window);
25});25});
26"/>26"/>
2727
=== added file 'lib/lp/app/templates/text-area-editor.pt'
--- lib/lp/app/templates/text-area-editor.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/templates/text-area-editor.pt 2011-01-30 20:24:55 +0000
@@ -0,0 +1,40 @@
1<div tal:attributes="id view/content_box_id;
2 class view/tag_class">
3 <div class="clearfix">
4 <div class="edit-controls" tal:condition="view/can_write">
5 &nbsp;
6 <a tal:attributes="href view/edit_url"
7 class="yui3-editable_text-trigger sprite edit"></a>
8 </div>
9 <h2 tal:condition="view/title"
10 tal:content="view/title">the title</h2>
11 </div>
12 <div class="yui3-editable_text-text"
13 tal:content="structure view/value">some text</div>
14<script tal:condition="view/can_write"
15 tal:content="structure string:
16 LPS.use('lazr.editor', 'lp.client.plugins', function (Y) {
17 var widget = new Y.EditableText({
18 contentBox: ${view/widget_css_selector},
19 accept_empty: ${view/accept_empty},
20 multiline: true,
21 buttons: 'top'
22 });
23 widget.editor.plug({
24 fn: Y.lp.client.plugins.PATCHPlugin, cfg: {
25 patch: ${view/json_attribute},
26 resource: ${view/json_attribute_uri},
27 patch_field: true,
28 accept: 'application/xhtml+xml'
29 }});
30 if (!Y.UA.opera) {
31 widget.render();
32 }
33 var lpns = Y.namespace('lp');
34 if (!lpns.widgets) {
35 lpns.widgets = {};
36 }
37 lpns.widgets['${view/content_box_id}'] = widget;
38 });
39"/>
40</div>
041
=== added file 'lib/lp/app/templates/text-line-editor.pt'
--- lib/lp/app/templates/text-line-editor.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/templates/text-line-editor.pt 2011-01-30 20:24:55 +0000
@@ -0,0 +1,25 @@
1<tal:open-tag replace="structure view/open_tag"/><span
2 class="yui3-editable_text-text"><tal:text replace="view/value"/></span>
3 <a tal:condition="view/can_write"
4 tal:attributes="href view/edit_url"
5 class="yui3-editable_text-trigger sprite edit"></a>
6<tal:close-tag replace="structure view/close_tag"/>
7
8<script tal:condition="view/can_write"
9 tal:content="structure string:
10 LPS.use('lazr.editor', 'lp.client.plugins', function (Y) {
11 var widget = new Y.EditableText({
12 contentBox: ${view/widget_css_selector},
13 accept_empty: ${view/accept_empty},
14 width: ${view/width},
15 initial_value_override: ${view/initial_value_override}
16 });
17 widget.editor.plug({
18 fn: Y.lp.client.plugins.PATCHPlugin, cfg: {
19 patch: ${view/json_attribute},
20 resource: ${view/json_attribute_uri},
21 patch_field: true
22 }});
23 widget.render();
24 });
25"/>
026
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-01-18 20:29:21 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-01-30 20:24:55 +0000
@@ -163,11 +163,6 @@
163from canonical.lazr.interfaces import IObjectPrivacy163from canonical.lazr.interfaces import IObjectPrivacy
164from canonical.lazr.utils import smartquote164from canonical.lazr.utils import smartquote
165from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget165from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
166from canonical.widgets.lazrjs import (
167 TextAreaEditorWidget,
168 TextLineEditorWidget,
169 vocabulary_to_choice_edit_items,
170 )
171from canonical.widgets.project import ProjectScopeWidget166from canonical.widgets.project import ProjectScopeWidget
172from lp.answers.interfaces.questiontarget import IQuestionTarget167from lp.answers.interfaces.questiontarget import IQuestionTarget
173from lp.app.browser.launchpadform import (168from lp.app.browser.launchpadform import (
@@ -176,8 +171,12 @@
176 LaunchpadEditFormView,171 LaunchpadEditFormView,
177 LaunchpadFormView,172 LaunchpadFormView,
178 )173 )
174from lp.app.browser.lazrjs import (
175 TextAreaEditorWidget,
176 TextLineEditorWidget,
177 vocabulary_to_choice_edit_items,
178 )
179from lp.app.browser.tales import (179from lp.app.browser.tales import (
180 FormattersAPI,
181 ObjectImageDisplayAPI,180 ObjectImageDisplayAPI,
182 PersonFormatterAPI,181 PersonFormatterAPI,
183 )182 )
@@ -679,8 +678,8 @@
679 canonical_url(self.context.bug.default_bugtask))678 canonical_url(self.context.bug.default_bugtask))
680679
681 self.bug_title_edit_widget = TextLineEditorWidget(680 self.bug_title_edit_widget = TextLineEditorWidget(
682 bug, 'title', canonical_url(self.context, view_name='+edit'),681 bug, IBug['title'], "Edit this summary", 'h1',
683 id="bug-title", title="Edit this summary")682 edit_url=canonical_url(self.context, view_name='+edit'))
684683
685 # XXX 2010-10-05 gmb bug=655597:684 # XXX 2010-10-05 gmb bug=655597:
686 # This line of code keeps the view's query count down,685 # This line of code keeps the view's query count down,
@@ -1035,16 +1034,12 @@
1035 @property1034 @property
1036 def bug_description_html(self):1035 def bug_description_html(self):
1037 """The bug's description as HTML."""1036 """The bug's description as HTML."""
1038 formatter = FormattersAPI1037 bug = self.context.bug
1039 hide_email = formatter(self.context.bug.description).obfuscate_email()1038 description = IBug['description']
1040 description = formatter(hide_email).text_to_html()1039 title = "Bug Description"
1040 edit_url = canonical_url(self.context, view_name='+edit')
1041 return TextAreaEditorWidget(1041 return TextAreaEditorWidget(
1042 self.context.bug,1042 bug, description, title, edit_url=edit_url)
1043 'description',
1044 canonical_url(self.context, view_name='+edit'),
1045 id="edit-description",
1046 title="Bug Description",
1047 value=description)
10481043
1049 @property1044 @property
1050 def bug_heat_html(self):1045 def bug_heat_html(self):
10511046
=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt 2010-12-21 15:07:26 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt 2011-01-30 20:24:55 +0000
@@ -103,9 +103,7 @@
103 <tal:description103 <tal:description
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" />
105105
106 <div id="edit-description" class="lazr-multiline-edit"106 <tal:widget replace="structure view/bug_description_html"/>
107 tal:content="structure view/bug_description_html"
108 />
109107
110 <div style="margin:-20px 0 20px 5px" class="clearfix">108 <div style="margin:-20px 0 20px 5px" class="clearfix">
111 <span tal:condition="view/wasDescriptionModified" class="discreet"109 <span tal:condition="view/wasDescriptionModified" class="discreet"
112110
=== modified file 'lib/lp/bugs/windmill/tests/test_mark_duplicate.py'
--- lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2010-11-23 14:18:07 +0000
+++ lib/lp/bugs/windmill/tests/test_mark_duplicate.py 2011-01-30 20:24:55 +0000
@@ -115,7 +115,7 @@
115 client.click(link=u'bug #1')115 client.click(link=u'bug #1')
116 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)116 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
117 client.waits.forElement(117 client.waits.forElement(
118 id=u'bug-title', timeout=constants.FOR_ELEMENT)118 id=u'edit-title', timeout=constants.FOR_ELEMENT)
119119
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.
121 client.waits.forElement(121 client.waits.forElement(
122122
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2011-01-28 02:16:33 +0000
+++ lib/lp/code/browser/branch.py 2011-01-30 20:24:55 +0000
@@ -100,13 +100,13 @@
100from canonical.lazr.utils import smartquote100from canonical.lazr.utils import smartquote
101from canonical.widgets.suggestion import TargetBranchWidget101from canonical.widgets.suggestion import TargetBranchWidget
102from canonical.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription102from canonical.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
103from canonical.widgets.lazrjs import vocabulary_to_choice_edit_items
104from lp.app.browser.launchpadform import (103from lp.app.browser.launchpadform import (
105 action,104 action,
106 custom_widget,105 custom_widget,
107 LaunchpadEditFormView,106 LaunchpadEditFormView,
108 LaunchpadFormView,107 LaunchpadFormView,
109 )108 )
109from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
110from lp.app.errors import NotFoundError110from lp.app.errors import NotFoundError
111from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch111from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
112from lp.bugs.interfaces.bug import IBugSet112from lp.bugs.interfaces.bug import IBugSet
113113
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2011-01-18 20:49:35 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2011-01-30 20:24:55 +0000
@@ -87,12 +87,11 @@
87 LaunchpadEditFormView,87 LaunchpadEditFormView,
88 LaunchpadFormView,88 LaunchpadFormView,
89 )89 )
90from lp.app.browser.tales import DateTimeFormatterAPI90from lp.app.browser.lazrjs import (
91from canonical.widgets.lazrjs import (
92 TextAreaEditorWidget,91 TextAreaEditorWidget,
93 vocabulary_to_choice_edit_items,92 vocabulary_to_choice_edit_items,
94 )93 )
95from lp.app.browser.stringformatter import FormattersAPI94from lp.app.browser.tales import DateTimeFormatterAPI
96from lp.code.adapters.branch import BranchMergeProposalDelta95from lp.code.adapters.branch import BranchMergeProposalDelta
97from lp.code.browser.codereviewcomment import CodeReviewDisplayComment96from lp.code.browser.codereviewcomment import CodeReviewDisplayComment
98from lp.code.browser.decorations import (97from lp.code.browser.decorations import (
@@ -691,40 +690,36 @@
691 for bug in self.context.related_bugs]690 for bug in self.context.related_bugs]
692691
693 @property692 @property
693 def edit_description_link_class(self):
694 if self.context.description:
695 return "unseen"
696 else:
697 return ""
698
699 @property
694 def description_html(self):700 def description_html(self):
695 """The description as widget HTML."""701 """The description as widget HTML."""
696 description = self.context.description702 mp = self.context
697 if description is None:703 description = IBranchMergeProposal['description']
698 description = ''704 title = "Description of the Change"
699 formatter = FormattersAPI
700 hide_email = formatter(description).obfuscate_email()
701 description = formatter(hide_email).text_to_html()
702 return TextAreaEditorWidget(705 return TextAreaEditorWidget(
703 self.context,706 mp, description, title, edit_view='+edit-description')
704 'description',707
705 canonical_url(self.context, view_name='+edit-description'),708 @property
706 id="edit-description",709 def edit_commit_message_link_class(self):
707 title="Description of the Change",710 if self.context.commit_message:
708 value=description,711 return "unseen"
709 accept_empty=True)712 else:
713 return ""
710714
711 @property715 @property
712 def commit_message_html(self):716 def commit_message_html(self):
713 """The commit message as widget HTML."""717 """The commit message as widget HTML."""
714 commit_message = self.context.commit_message718 mp = self.context
715 if commit_message is None:719 commit_message = IBranchMergeProposal['commit_message']
716 commit_message = ''720 title = "Commit Message"
717 formatter = FormattersAPI
718 hide_email = formatter(commit_message).obfuscate_email()
719 commit_message = formatter(hide_email).text_to_html()
720 return TextAreaEditorWidget(721 return TextAreaEditorWidget(
721 self.context,722 mp, commit_message, title, edit_view='+edit-commit-message')
722 'commit_message',
723 canonical_url(self.context, view_name='+edit-commit-message'),
724 id="edit-commit_message",
725 title="Commit Message",
726 value=commit_message,
727 accept_empty=True)
728723
729 @property724 @property
730 def status_config(self):725 def status_config(self):
731726
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-01-20 20:50:45 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-01-30 20:24:55 +0000
@@ -62,7 +62,6 @@
62 LabeledMultiCheckBoxWidget,62 LabeledMultiCheckBoxWidget,
63 LaunchpadRadioWidget,63 LaunchpadRadioWidget,
64 )64 )
65from canonical.widgets.lazrjs import InlineEditPickerWidget
66from canonical.widgets.suggestion import RecipeOwnerWidget65from canonical.widgets.suggestion import RecipeOwnerWidget
67from lp.app.browser.launchpadform import (66from lp.app.browser.launchpadform import (
68 action,67 action,
@@ -72,9 +71,8 @@
72 LaunchpadFormView,71 LaunchpadFormView,
73 render_radio_widget_part,72 render_radio_widget_part,
74 )73 )
75from lp.app.browser.tales import (74from lp.app.browser.lazrjs import InlineEditPickerWidget
76 format_link,75from lp.app.browser.tales import format_link
77 )
78from lp.code.errors import (76from lp.code.errors import (
79 BuildAlreadyPending,77 BuildAlreadyPending,
80 NoSuchBranch,78 NoSuchBranch,
@@ -235,9 +233,8 @@
235 @property233 @property
236 def person_picker(self):234 def person_picker(self):
237 return InlineEditPickerWidget(235 return InlineEditPickerWidget(
238 self.context, self.request, ISourcePackageRecipe['owner'],236 self.context, ISourcePackageRecipe['owner'],
239 format_link(self.context.owner),237 format_link(self.context.owner),
240 content_box_id='recipe-owner',
241 header='Change owner',238 header='Change owner',
242 step_title='Select a new owner')239 step_title='Select a new owner')
243240
@@ -248,11 +245,9 @@
248 initial_html = 'None'245 initial_html = 'None'
249 else:246 else:
250 initial_html = format_link(ppa)247 initial_html = format_link(ppa)
248 field = ISourcePackageEditSchema['daily_build_archive']
251 return InlineEditPickerWidget(249 return InlineEditPickerWidget(
252 self.context, self.request,250 self.context, field, initial_html,
253 ISourcePackageAddSchema['daily_build_archive'],
254 initial_html,
255 content_box_id='recipe-ppa',
256 header='Change daily build archive',251 header='Change daily build archive',
257 step_title='Select a PPA')252 step_title='Select a PPA')
258253
259254
=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt 2010-10-10 21:54:16 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt 2011-01-30 20:24:55 +0000
@@ -95,45 +95,23 @@
95 </div>95 </div>
9696
97 <div id="commit-message" class="yui-g">97 <div id="commit-message" class="yui-g">
98 <tal:no-commit-message condition="not: context/commit_message">98 <div tal:define="link menu/set_commit_message"
99 <div tal:define="link menu/set_commit_message"99 tal:condition="link/enabled"
100 tal:condition="link/enabled"100 tal:content="structure link/render"
101 tal:content="structure link/render">101 tal:attributes="class view/edit_commit_message_link_class">
102 Set commit message102 Set commit message
103 </div>103 </div>
104 <div id="edit-commit_message" class="lazr-multiline-edit unseen"104 <tal:widget replace="structure view/commit_message_html"/>
105 tal:content="structure view/commit_message_html"/>
106 </tal:no-commit-message>
107 <tal:has-commit-message condition="context/commit_message">
108 <div tal:define="link menu/set_commit_message"
109 tal:condition="link/enabled"
110 tal:content="structure link/render" class="unseen">
111 Set commit message
112 </div>
113 <div id="edit-commit_message" class="lazr-multiline-edit"
114 tal:content="structure view/commit_message_html"/>
115 </tal:has-commit-message>
116 </div>105 </div>
117106
118 <div id="description" class="yui-g">107 <div id="description" class="yui-g">
119 <tal:no-description condition="not: context/description">108 <div tal:define="link menu/set_description"
120 <div tal:define="link menu/set_description"109 tal:condition="link/enabled"
121 tal:condition="link/enabled"110 tal:content="structure link/render"
122 tal:content="structure link/render">111 tal:attributes="class view/edit_description_link_class">
123 Set description112 Set description
124 </div>113 </div>
125 <div id="edit-description" class="lazr-multiline-edit unseen"114 <tal:widget replace="structure view/description_html"/>
126 tal:content="structure view/description_html"/>
127 </tal:no-description>
128 <tal:has-description condition="context/description">
129 <div tal:define="link menu/set_description"
130 tal:condition="link/enabled"
131 tal:content="structure link/render" class="unseen">
132 Set description
133 </div>
134 <div id="edit-description" class="lazr-multiline-edit"
135 tal:content="structure view/description_html"/>
136 </tal:has-description>
137 </div>115 </div>
138116
139 <div class="yui-g" tal:condition="view/has_bug_or_spec">117 <div class="yui-g" tal:condition="view/has_bug_or_spec">
140118
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2011-01-20 21:41:13 +0000
+++ lib/lp/registry/browser/product.py 2011-01-30 20:24:55 +0000
@@ -122,7 +122,6 @@
122 CheckBoxMatrixWidget,122 CheckBoxMatrixWidget,
123 LaunchpadRadioWidget,123 LaunchpadRadioWidget,
124 )124 )
125from canonical.widgets.lazrjs import TextLineEditorWidget
126from canonical.widgets.popup import PersonPickerWidget125from canonical.widgets.popup import PersonPickerWidget
127from canonical.widgets.product import (126from canonical.widgets.product import (
128 GhostWidget,127 GhostWidget,
@@ -143,6 +142,7 @@
143 ReturnToReferrerMixin,142 ReturnToReferrerMixin,
144 safe_action,143 safe_action,
145 )144 )
145from lp.app.browser.lazrjs import TextLineEditorWidget
146from lp.app.browser.tales import MenuAPI146from lp.app.browser.tales import MenuAPI
147from lp.app.enums import ServiceUsage147from lp.app.enums import ServiceUsage
148from lp.app.errors import NotFoundError148from lp.app.errors import NotFoundError
@@ -988,25 +988,21 @@
988988
989 def initialize(self):989 def initialize(self):
990 self.status_message = None990 self.status_message = None
991 product = self.context
992 title_field = IProduct['title']
993 title = "Edit this title"
991 self.title_edit_widget = TextLineEditorWidget(994 self.title_edit_widget = TextLineEditorWidget(
992 self.context, 'title',995 product, title_field, title, 'h1')
993 canonical_url(self.context, view_name='+edit'),996 programming_lang = IProduct['programminglang']
994 id="product-title", title="Edit this title")997 title = 'Edit programming languages'
998 additional_arguments = {'width': '9em'}
995 if self.context.programminglang is None:999 if self.context.programminglang is None:
996 additional_arguments = dict(1000 additional_arguments.update(dict(
997 default_text='Not yet specified',1001 default_text='Not yet specified',
998 initial_value_override='',1002 initial_value_override='',
999 )1003 ))
1000 else:
1001 additional_arguments = {}
1002 self.languages_edit_widget = TextLineEditorWidget(1004 self.languages_edit_widget = TextLineEditorWidget(
1003 self.context, 'programminglang',1005 product, programming_lang, title, 'span', **additional_arguments)
1004 canonical_url(self.context, view_name='+edit'),
1005 id='programminglang', title='Edit programming languages',
1006 tag='span', public_attribute='programming_language',
1007 accept_empty=True,
1008 width='9em',
1009 **additional_arguments)
1010 self.show_programming_languages = bool(1006 self.show_programming_languages = bool(
1011 self.context.programminglang or1007 self.context.programminglang or
1012 check_permission('launchpad.Edit', self.context))1008 check_permission('launchpad.Edit', self.context))
10131009
=== modified file 'lib/lp/registry/windmill/tests/test_product.py'
--- lib/lp/registry/windmill/tests/test_product.py 2010-10-18 12:56:47 +0000
+++ lib/lp/registry/windmill/tests/test_product.py 2011-01-30 20:24:55 +0000
@@ -24,7 +24,7 @@
24 def test_title_inline_edit(self):24 def test_title_inline_edit(self):
25 test = widgets.InlineEditorWidgetTest(25 test = widgets.InlineEditorWidgetTest(
26 url='%s/firefox' % RegistryWindmillLayer.base_url,26 url='%s/firefox' % RegistryWindmillLayer.base_url,
27 widget_id='product-title',27 widget_id='edit-title',
28 expected_value='Mozilla Firefox',28 expected_value='Mozilla Firefox',
29 new_value='The awesome Mozilla Firefox',29 new_value='The awesome Mozilla Firefox',
30 name='test_title_inline_edit',30 name='test_title_inline_edit',
@@ -35,7 +35,7 @@
35 def test_programming_languages_edit(self):35 def test_programming_languages_edit(self):
36 test = widgets.InlineEditorWidgetTest(36 test = widgets.InlineEditorWidgetTest(
37 url='%s/firefox' % RegistryWindmillLayer.base_url,37 url='%s/firefox' % RegistryWindmillLayer.base_url,
38 widget_id='programminglang',38 widget_id='edit-programminglang',
39 widget_tag='span',39 widget_tag='span',
40 expected_value='Not yet specified',40 expected_value='Not yet specified',
41 new_value='C++',41 new_value='C++',
4242
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2011-01-29 07:41:50 +0000
+++ lib/lp/soyuz/browser/archive.py 2011-01-30 20:24:55 +0000
@@ -87,10 +87,6 @@
87 LaunchpadDropdownWidget,87 LaunchpadDropdownWidget,
88 LaunchpadRadioWidget,88 LaunchpadRadioWidget,
89 )89 )
90from canonical.widgets.lazrjs import (
91 TextAreaEditorWidget,
92 TextLineEditorWidget,
93 )
94from canonical.widgets.textwidgets import StrippedTextWidget90from canonical.widgets.textwidgets import StrippedTextWidget
95from lp.app.browser.launchpadform import (91from lp.app.browser.launchpadform import (
96 action,92 action,
@@ -98,6 +94,10 @@
98 LaunchpadEditFormView,94 LaunchpadEditFormView,
99 LaunchpadFormView,95 LaunchpadFormView,
100 )96 )
97from lp.app.browser.lazrjs import (
98 TextAreaEditorWidget,
99 TextLineEditorWidget,
100 )
101from lp.app.browser.stringformatter import FormattersAPI101from lp.app.browser.stringformatter import FormattersAPI
102from lp.app.errors import NotFoundError102from lp.app.errors import NotFoundError
103from lp.buildmaster.enums import BuildStatus103from lp.buildmaster.enums import BuildStatus
@@ -863,11 +863,9 @@
863863
864 @property864 @property
865 def displayname_edit_widget(self):865 def displayname_edit_widget(self):
866 widget = TextLineEditorWidget(866 display_name = IArchive['displayname']
867 self.context, 'displayname',867 title = "Edit the displayname"
868 canonical_url(self.context, view_name='+edit'),868 return TextLineEditorWidget(self.context, display_name, title, 'h1')
869 id="displayname", title="Edit the displayname")
870 return widget
871869
872 @property870 @property
873 def sources_list_entries(self):871 def sources_list_entries(self):
@@ -897,25 +895,17 @@
897 @property895 @property
898 def archive_description_html(self):896 def archive_description_html(self):
899 """The archive's description as HTML."""897 """The archive's description as HTML."""
900 formatter = FormattersAPI898 linkify_text = True
901
902 description = self.context.description
903 if description is not None:
904 description = formatter(description).obfuscate_email()
905 else:
906 description = ''
907
908 if self.context.is_ppa:899 if self.context.is_ppa:
909 description = formatter(description).text_to_html(900 linkify_text = not self.context.owner.is_probationary
910 linkify_text=(not self.context.owner.is_probationary))901 archive = self.context
911902 description = IArchive['description']
903 title = self.archive_label + " description"
904 # Don't hide empty archive descriptions. Even though the interface
905 # says they are required, the model doesn't.
912 return TextAreaEditorWidget(906 return TextAreaEditorWidget(
913 self.context,907 archive, description, title, hide_empty=False,
914 'description',908 linkify_text=linkify_text)
915 canonical_url(self.context, view_name='+edit'),
916 id="edit-description",
917 title=self.archive_label + " description",
918 value=description)
919909
920 @cachedproperty910 @cachedproperty
921 def latest_updates(self):911 def latest_updates(self):
922912
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt'
--- lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2010-10-17 15:44:08 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2011-01-30 20:24:55 +0000
@@ -199,8 +199,9 @@
199 Copy archive disabled-security-rebuild for No Privileges Person : Ubuntu199 Copy archive disabled-security-rebuild for No Privileges Person : Ubuntu
200200
201 >>> main_content = find_main_content(nopriv_browser.contents)201 >>> main_content = find_main_content(nopriv_browser.contents)
202 >>> print main_content.h1202 >>> print main_content.h1['class']
203 <h1 id="displayname">...</h1>203 Traceback (most recent call last):
204 KeyError: 'class'
204205
205 >>> print first_tag_by_class(nopriv_browser.contents, 'warning message')206 >>> print first_tag_by_class(nopriv_browser.contents, 'warning message')
206 None207 None
207208
=== modified file 'lib/lp/soyuz/templates/archive-index.pt'
--- lib/lp/soyuz/templates/archive-index.pt 2010-11-10 15:33:47 +0000
+++ lib/lp/soyuz/templates/archive-index.pt 2011-01-30 20:24:55 +0000
@@ -31,9 +31,7 @@
31 This archive has been disabled.31 This archive has been disabled.
32 </p>32 </p>
3333
34 <div id="edit-description" class="lazr-multiline-edit"34 <tal:widget replace="structure view/archive_description_html"/>
35 tal:content="structure view/archive_description_html"
36 />
37 </div>35 </div>
3836
39 <tal:ppa-upload-hint condition="context/is_ppa">37 <tal:ppa-upload-hint condition="context/is_ppa">
4038
=== modified file 'lib/lp/soyuz/windmill/tests/test_ppainlineedit.py'
--- lib/lp/soyuz/windmill/tests/test_ppainlineedit.py 2010-10-18 12:56:47 +0000
+++ lib/lp/soyuz/windmill/tests/test_ppainlineedit.py 2011-01-30 20:24:55 +0000
@@ -20,7 +20,7 @@
2020
21 ppa_displayname_inline_edit_test = widgets.InlineEditorWidgetTest(21 ppa_displayname_inline_edit_test = widgets.InlineEditorWidgetTest(
22 url='%s/~cprov/+archive/ppa' % SoyuzWindmillLayer.base_url,22 url='%s/~cprov/+archive/ppa' % SoyuzWindmillLayer.base_url,
23 widget_id='displayname',23 widget_id='edit-displayname',
24 expected_value='PPA for Celso Providelo',24 expected_value='PPA for Celso Providelo',
25 new_value="Celso's default PPA",25 new_value="Celso's default PPA",
26 name='test_ppa_displayname_inline_edit',26 name='test_ppa_displayname_inline_edit',
2727
=== modified file 'lib/lp/translations/browser/hastranslationimports.py'
--- lib/lp/translations/browser/hastranslationimports.py 2010-11-23 23:22:27 +0000
+++ lib/lp/translations/browser/hastranslationimports.py 2011-01-30 20:24:55 +0000
@@ -29,13 +29,13 @@
29from canonical.launchpad.webapp.authorization import check_permission29from canonical.launchpad.webapp.authorization import check_permission
30from canonical.launchpad.webapp.batching import TableBatchNavigator30from canonical.launchpad.webapp.batching import TableBatchNavigator
31from canonical.launchpad.webapp.vocabulary import ForgivingSimpleVocabulary31from canonical.launchpad.webapp.vocabulary import ForgivingSimpleVocabulary
32from canonical.widgets.lazrjs import vocabulary_to_choice_edit_items
33from lp.app.browser.launchpadform import (32from lp.app.browser.launchpadform import (
34 action,33 action,
35 custom_widget,34 custom_widget,
36 LaunchpadFormView,35 LaunchpadFormView,
37 safe_action,36 safe_action,
38 )37 )
38from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
39from lp.app.errors import UnexpectedFormData39from lp.app.errors import UnexpectedFormData
40from lp.registry.interfaces.distribution import IDistribution40from lp.registry.interfaces.distribution import IDistribution
41from lp.registry.interfaces.pillar import IPillarNameSet41from lp.registry.interfaces.pillar import IPillarNameSet