1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
|
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Wrappers for lazr-js widgets."""
__metaclass__ = type
__all__ = [
'BooleanChoiceWidget',
'EnumChoiceWidget',
'InlineEditPickerWidget',
'InlinePersonEditPickerWidget',
'InlineMultiCheckboxWidget',
'standard_text_html_representation',
'TextAreaEditorWidget',
'TextLineEditorWidget',
'vocabulary_to_choice_edit_items',
]
import simplejson
from lazr.enum import IEnumeratedType
from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
from lazr.restful.utils import (
get_current_browser_request,
safe_hasattr,
)
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.component import getUtility
from zope.security.checker import canAccess, canWrite
from zope.schema.interfaces import (
ICollection,
IVocabulary,
)
from zope.schema.vocabulary import getVocabularyRegistry
from canonical.launchpad.webapp.interfaces import ILaunchBag
from canonical.launchpad.webapp.publisher import canonical_url
from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.browser.vocabulary import get_person_picker_entry_metadata
from lp.services.propertycache import cachedproperty
class WidgetBase:
"""Useful methods for all widgets."""
def __init__(self, context, exported_field, content_box_id,
edit_view, edit_url, edit_title):
self.context = context
self.exported_field = exported_field
self.request = get_current_browser_request()
self.attribute_name = exported_field.__name__
self.optional_field = not exported_field.required
if content_box_id is None:
content_box_id = "edit-%s" % self.attribute_name
self.content_box_id = content_box_id
if edit_url is None:
edit_url = canonical_url(self.context, view_name=edit_view)
self.edit_url = edit_url
if edit_title is None:
edit_title = ''
self.edit_title = edit_title
# The mutator method name is used to determine whether or not the
# current user has permission to alter the attribute if the attribute
# is using a mutator function.
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__
self.json_attribute = simplejson.dumps(self.api_attribute)
@property
def resource_uri(self):
"""A local path to the context object.
The javascript uses the normalize_uri method that adds the appropriate
prefix to the uri. Doing it this way avoids needing to adapt the
current request into a webservice request in order to get an api url.
"""
return canonical_url(self.context, force_local_path=True)
@property
def json_resource_uri(self):
return simplejson.dumps(self.resource_uri)
@property
def can_write(self):
"""Can the current user write to the attribute."""
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
class TextWidgetBase(WidgetBase):
"""Abstract base for the single and multiline text editor widgets."""
def __init__(self, context, exported_field, title, content_box_id,
edit_view, edit_url, edit_title):
super(TextWidgetBase, self).__init__(
context, exported_field, content_box_id,
edit_view, edit_url, edit_title)
self.accept_empty = simplejson.dumps(self.optional_field)
self.title = title
self.widget_css_selector = simplejson.dumps('#' + self.content_box_id)
@property
def json_attribute_uri(self):
return simplejson.dumps(self.resource_uri + '/' + self.api_attribute)
class DefinedTagMixin:
"""Mixin class to define open and closing tags."""
@property
def open_tag(self):
return '<%s id="%s">' % (self.tag, self.content_box_id)
@property
def close_tag(self):
return '</%s>' % self.tag
class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
"""Wrapper for the lazr-js inlineedit/editor.js widget."""
__call__ = ViewPageTemplateFile('../templates/text-line-editor.pt')
def __init__(self, context, exported_field, title, tag,
content_box_id=None, edit_view="+edit", edit_url=None,
edit_title='',
default_text=None, initial_value_override=None, width=None):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param title: The string to use as the link title.
:param tag: The HTML tag to use.
:param content_box_id: The HTML id to use for this widget.
Defaults to edit-<attribute name>.
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
:param default_text: Text to show in the unedited field, if the
parameter value is missing or None.
:param initial_value_override: Use this text for the initial edited
field value instead of the attribute's current value.
:param width: Initial widget width.
"""
super(TextLineEditorWidget, self).__init__(
context, exported_field, title, content_box_id,
edit_view, edit_url, edit_title)
self.tag = tag
self.default_text = default_text
self.initial_value_override = simplejson.dumps(initial_value_override)
self.width = simplejson.dumps(width)
@property
def value(self):
text = getattr(self.context, self.attribute_name, self.default_text)
if text is None:
return self.default_text
else:
return FormattersAPI(text).obfuscate_email()
class TextAreaEditorWidget(TextWidgetBase):
"""Wrapper for the multine-line lazr-js inlineedit/editor.js widget."""
__call__ = ViewPageTemplateFile('../templates/text-area-editor.pt')
def __init__(self, context, exported_field, title, content_box_id=None,
edit_view="+edit", edit_url=None, edit_title='',
hide_empty=True, linkify_text=True):
"""Create the widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param title: The string to use as the link title.
:param content_box_id: The HTML id to use for this widget.
Defaults to edit-<attribute name>.
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
:param hide_empty: If the attribute has no value, or is empty, then
hide the editor by adding the "unseen" CSS class.
:param linkify_text: If True the HTML version of the text will have
things that look like links made into anchors.
"""
super(TextAreaEditorWidget, self).__init__(
context, exported_field, title, content_box_id,
edit_view, edit_url, edit_title)
self.hide_empty = hide_empty
self.linkify_text = linkify_text
@property
def tag_class(self):
"""The CSS class for the widget."""
classes = ['lazr-multiline-edit']
if self.hide_empty and not self.value:
classes.append('unseen')
return ' '.join(classes)
@cachedproperty
def value(self):
text = getattr(self.context, self.attribute_name, None)
return standard_text_html_representation(text, self.linkify_text)
class InlineEditPickerWidget(WidgetBase):
"""Wrapper for the lazr-js picker widget.
This widget is not for editing form values like the
VocabularyPickerWidget.
"""
__call__ = ViewPageTemplateFile('../templates/inline-picker.pt')
def __init__(self, context, exported_field, default_html,
content_box_id=None, header='Select an item',
step_title='Search',
null_display_value='None',
edit_view="+edit", edit_url=None, edit_title='',
help_link=None):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param default_html: Default display of attribute.
:param content_box_id: The HTML id to use for this widget.
Automatically generated if this is not provided.
:param header: The large text at the top of the picker.
:param step_title: Smaller line of text below the header.
:param null_display_value: This will be shown for a missing value
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
"""
super(InlineEditPickerWidget, self).__init__(
context, exported_field, content_box_id,
edit_view, edit_url, edit_title)
self.default_html = default_html
self.header = header
self.step_title = step_title
self.null_display_value = null_display_value
self.help_link = help_link
# JSON encoded attributes.
self.json_content_box_id = simplejson.dumps(self.content_box_id)
self.json_attribute = simplejson.dumps(self.api_attribute + '_link')
self.json_vocabulary_name = simplejson.dumps(
self.exported_field.vocabularyName)
@property
def picker_type(self):
return 'default'
@property
def selected_value_metadata(self):
return None
@property
def selected_value(self):
""" String representation of field value associated with the picker.
Default implementation is to return the 'name' attribute.
"""
if self.context is None:
return None
val = getattr(self.context, self.exported_field.__name__)
if val is not None and safe_hasattr(val, 'name'):
return getattr(val, 'name')
return None
@property
def config(self):
return self.getConfig()
def getConfig(self):
return dict(
picker_type=self.picker_type,
header=self.header, step_title=self.step_title,
selected_value=self.selected_value,
selected_value_metadata=self.selected_value_metadata,
null_display_value=self.null_display_value,
show_search_box=self.show_search_box,
vocabulary_filters=self.vocabulary_filters)
@property
def json_config(self):
return simplejson.dumps(self.config)
@cachedproperty
def vocabulary(self):
registry = getVocabularyRegistry()
return registry.get(
IVocabulary, self.exported_field.vocabularyName)
@cachedproperty
def vocabulary_filters(self):
# Only IHugeVocabulary's have filters.
if not IHugeVocabulary.providedBy(self.vocabulary):
return []
supported_filters = self.vocabulary.supportedFilters()
# If we have no filters or just the ALL filter, then no filtering
# support is required.
filters = []
if (len(supported_filters) == 0 or
(len(supported_filters) == 1
and supported_filters[0].name == 'ALL')):
return filters
for filter in supported_filters:
filters.append({
'name': filter.name,
'title': filter.title,
'description': filter.description,
})
return filters
@property
def show_search_box(self):
return IHugeVocabulary.providedBy(self.vocabulary)
class InlinePersonEditPickerWidget(InlineEditPickerWidget):
def __init__(self, context, exported_field, default_html,
content_box_id=None, header='Select an item',
step_title='Search', assign_me_text='Pick me',
remove_person_text='Remove person',
remove_team_text='Remove team',
null_display_value='None',
edit_view="+edit", edit_url=None, edit_title='',
help_link=None):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param default_html: Default display of attribute.
:param content_box_id: The HTML id to use for this widget.
Automatically generated if this is not provided.
:param header: The large text at the top of the picker.
:param step_title: Smaller line of text below the header.
:param assign_me_text: Override default button text: "Pick me"
:param remove_person_text: Override default link text: "Remove person"
:param remove_team_text: Override default link text: "Remove team"
:param null_display_value: This will be shown for a missing value
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
"""
super(InlinePersonEditPickerWidget, self).__init__(
context, exported_field, default_html, content_box_id, header,
step_title, null_display_value,
edit_view, edit_url, edit_title, help_link)
self.assign_me_text = assign_me_text
self.remove_person_text = remove_person_text
self.remove_team_text = remove_team_text
@property
def picker_type(self):
return 'person'
@property
def selected_value_metadata(self):
val = getattr(self.context, self.exported_field.__name__)
return get_person_picker_entry_metadata(val)
@property
def show_assign_me_button(self):
# show_assign_me_button is true if user is in the vocabulary.
vocabulary = self.vocabulary
user = getUtility(ILaunchBag).user
return user and user in vocabulary
def getConfig(self):
config = super(InlinePersonEditPickerWidget, self).getConfig()
config.update(dict(
show_remove_button=self.optional_field,
show_assign_me_button=self.show_assign_me_button,
assign_me_text=self.assign_me_text,
remove_person_text=self.remove_person_text,
remove_team_text=self.remove_team_text))
return config
class InlineMultiCheckboxWidget(WidgetBase):
"""Wrapper for the lazr-js multicheckbox widget."""
__call__ = ViewPageTemplateFile(
'../templates/inline-multicheckbox-widget.pt')
def __init__(self, context, exported_field,
label, label_tag="span", attribute_type="default",
vocabulary=None, header=None,
empty_display_value="None", selected_items=list(),
items_tag="span", items_style='',
content_box_id=None, edit_view="+edit", edit_url=None,
edit_title=''):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param label: The label text to display above the checkboxes
:param label_tag: The tag in which to wrap the label text.
:param attribute_type: The attribute type. Currently only "reference"
is supported. Used to determine whether to linkify the selected
checkbox item values. So ubuntu/hoary becomes
http://launchpad.net/devel/api/ubuntu/hoary
:param vocabulary: The name of the vocabulary which provides the
items or a vocabulary instance.
:param header: The text to display as the title of the popup form.
:param empty_display_value: The text to display if no items are
selected.
:param selected_items: The currently selected items.
:param items_tag: The tag in which to wrap the items checkboxes.
:param items_style: The css style to use for each item checkbox.
:param content_box_id: The HTML id to use for this widget.
Automatically generated if this is not provided.
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
"""
super(InlineMultiCheckboxWidget, self).__init__(
context, exported_field, content_box_id,
edit_view, edit_url, edit_title)
linkify_items = attribute_type == "reference"
if header is None:
header = self.exported_field.title + ":"
self.header = header,
self.empty_display_value = empty_display_value
self.label = label
self.label_open_tag = "<%s>" % label_tag
self.label_close_tag = "</%s>" % label_tag
self.items = selected_items
self.items_open_tag = ("<%s id='%s'>" %
(items_tag, self.content_box_id + "-items"))
self.items_close_tag = "</%s>" % items_tag
self.linkify_items = linkify_items
if vocabulary is None:
if ICollection.providedBy(exported_field):
vocabulary = exported_field.value_type.vocabularyName
else:
vocabulary = exported_field.vocabularyName
if isinstance(vocabulary, basestring):
vocabulary = getVocabularyRegistry().get(context, vocabulary)
# Construct checkbox data dict for each item in the vocabulary.
items = []
style = ';'.join(['font-weight: normal', items_style])
for item in vocabulary:
item_value = item.value if safe_hasattr(item, 'value') else item
checked = item_value in selected_items
if linkify_items:
save_value = canonical_url(item_value, force_local_path=True)
else:
save_value = item_value.name
new_item = {
'name': item.title,
'token': item.token,
'style': style,
'checked': checked,
'value': save_value}
items.append(new_item)
self.has_choices = len(items)
# JSON encoded attributes.
self.json_content_box_id = simplejson.dumps(self.content_box_id)
self.json_attribute = simplejson.dumps(self.api_attribute)
self.json_attribute_type = simplejson.dumps(attribute_type)
self.json_items = simplejson.dumps(items)
self.json_description = simplejson.dumps(
self.exported_field.description)
@property
def config(self):
return dict(
header=self.header,
)
@property
def json_config(self):
return simplejson.dumps(self.config)
def vocabulary_to_choice_edit_items(
vocab, css_class_prefix=None, disabled_items=None, as_json=False,
name_fn=None, value_fn=None):
"""Convert an enumerable to JSON for a ChoiceEdit.
:vocab: The enumeration to iterate over.
:css_class_prefix: If present, append this to an item's value to create
the css_class property for it.
:disabled_items: A list of items that should be displayed, but disabled.
:name_fn: A function receiving an item and returning its name.
:value_fn: A function receiving an item and returning its value.
"""
if disabled_items is None:
disabled_items = []
items = []
for item in vocab:
# Introspect to make sure we're dealing with the object itself.
# SimpleTerm objects have the object itself at item.value.
if safe_hasattr(item, 'value'):
item = item.value
if name_fn is not None:
name = name_fn(item)
else:
name = item.title
if value_fn is not None:
value = value_fn(item)
else:
value = item.title
new_item = {
'name': name,
'value': value,
'style': '', 'help': '', 'disabled': False}
for disabled_item in disabled_items:
if disabled_item == item:
new_item['disabled'] = True
break
if css_class_prefix is not None:
new_item['css_class'] = css_class_prefix + item.name
items.append(new_item)
if as_json:
return simplejson.dumps(items)
else:
return items
def standard_text_html_representation(value, linkify_text=True):
"""Render a string for html display.
For this we obfuscate email and render as html.
"""
if value is None:
return ''
nomail = FormattersAPI(value).obfuscate_email()
return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text)
class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
"""A ChoiceEdit for a boolean field."""
__call__ = ViewPageTemplateFile('../templates/boolean-choice-widget.pt')
def __init__(self, context, exported_field,
tag, false_text, true_text, prefix=None,
edit_view="+edit", edit_url=None, edit_title='',
content_box_id=None, header='Select an item'):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param tag: The HTML tag to use.
:param false_text: The string to show for a false value.
:param true_text: The string to show for a true value.
:param prefix: Optional text to show before the value.
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
:param content_box_id: The HTML id to use for this widget.
Automatically generated if this is not provided.
:param header: The large text at the top of the choice popup.
"""
super(BooleanChoiceWidget, self).__init__(
context, exported_field, content_box_id,
edit_view, edit_url, edit_title)
self.header = header
self.tag = tag
self.prefix = prefix
self.true_text = true_text
self.false_text = false_text
self.current_value = getattr(self.context, self.attribute_name)
@property
def value(self):
if self.current_value:
return self.true_text
else:
return self.false_text
@property
def config(self):
return dict(
contentBox='#' + self.content_box_id,
value=self.current_value,
title=self.header,
items=[
dict(name=self.true_text, value=True, style='', help='',
disabled=False),
dict(name=self.false_text, value=False, style='', help='',
disabled=False)])
@property
def json_config(self):
return simplejson.dumps(self.config)
class EnumChoiceWidget(WidgetBase):
"""A popup choice widget."""
__call__ = ViewPageTemplateFile('../templates/enum-choice-widget.pt')
def __init__(self, context, exported_field, header,
content_box_id=None, enum=None,
edit_view="+edit", edit_url=None, edit_title='',
css_class_prefix=''):
"""Create a widget wrapper.
:param context: The object that is being edited.
:param exported_field: The attribute being edited. This should be
a field from an interface of the form ISomeInterface['fieldname']
:param header: The large text at the top of the picker.
:param content_box_id: The HTML id to use for this widget.
Automatically generated if this is not provided.
:param enum: The enumerated type used to generate the widget items.
:param edit_view: The view name to use to generate the edit_url if
one is not specified.
:param edit_url: The URL to use for editing when the user isn't logged
in and when JS is off. Defaults to the edit_view on the context.
:param edit_title: Used to set the title attribute of the anchor.
:param css_class_prefix: Added to the start of the enum titles.
"""
super(EnumChoiceWidget, self).__init__(
context, exported_field, content_box_id,
edit_view, edit_url, edit_title)
self.header = header
value = getattr(self.context, self.attribute_name)
self.css_class = "value %s%s" % (css_class_prefix, value.name)
self.value = value.title
if enum is None:
# Get the enum from the exported field.
enum = exported_field.vocabulary
if IEnumeratedType(enum, None) is None:
raise ValueError('%r does not provide IEnumeratedType' % enum)
self.items = vocabulary_to_choice_edit_items(enum, css_class_prefix)
@property
def config(self):
return dict(
contentBox='#' + self.content_box_id,
value=self.value,
title=self.header,
items=self.items)
@property
def json_config(self):
return simplejson.dumps(self.config)
|