closes #601987
1) sqlutils.restore_from_file have to use its confirm argument when
a command fail, to propose to continue there (this can't be handled
by the caller)
2) source.restore method hence needs to take this confirmation callback
as argument
3) properly fix places where 'drop' was given instead of 'confirm'
"""widget classes for form construction
:organization: Logilab
:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"
from datetime import date
from warnings import warn
from cubicweb.common import tags, uilib
from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE
from logilab.mtconverter import xml_escape
class FieldWidget(object):
"""abstract widget class"""
# javascript / css files required by the widget
needs_js = ()
needs_css = ()
# automatically set id and tabindex attributes ?
setdomid = True
settabindex = True
# does this widget expect a vocabulary
vocabulary_widget = False
def __init__(self, attrs=None, setdomid=None, settabindex=None):
if attrs is None:
attrs = {}
self.attrs = attrs
if setdomid is not None:
# override class's default value
self.setdomid = setdomid
if settabindex is not None:
# override class's default value
self.settabindex = settabindex
def add_media(self, form):
"""adds media (CSS & JS) required by this widget"""
if self.needs_js:
form.req.add_js(self.needs_js)
if self.needs_css:
form.req.add_css(self.needs_css)
def render(self, form, field, renderer):
"""render the widget for the given `field` of `form`.
To override in concrete class
"""
raise NotImplementedError
def _render_attrs(self, form, field):
"""return html tag name, attributes and a list of values for the field
"""
name = form.context[field]['name']
values = form.context[field]['value']
if not isinstance(values, (tuple, list)):
values = (values,)
attrs = dict(self.attrs)
if self.setdomid:
attrs['id'] = form.context[field]['id']
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form.req.next_tabindex()
return name, values, attrs
class Input(FieldWidget):
"""abstract widget class for <input> tag based widgets"""
type = None
def render(self, form, field, renderer):
"""render the widget for the given `field` of `form`.
Generate one <input> tag for each field's value
"""
self.add_media(form)
name, values, attrs = self._render_attrs(form, field)
# ensure something is rendered
if not values:
values = (INTERNAL_FIELD_VALUE,)
inputs = [tags.input(name=name, value=value, type=self.type, **attrs)
for value in values]
return u'\n'.join(inputs)
# basic html widgets ###########################################################
class TextInput(Input):
"""<input type='text'>"""
type = 'text'
class PasswordInput(Input):
"""<input type='password'> and its confirmation field (using
<field's name>-confirm as name)
"""
type = 'password'
def render(self, form, field, renderer):
self.add_media(form)
name, values, attrs = self._render_attrs(form, field)
assert len(values) == 1
id = attrs.pop('id')
try:
confirmname = '%s-confirm:%s' % tuple(name.rsplit(':', 1))
except TypeError:
confirmname = '%s-confirm' % name
inputs = [tags.input(name=name, value=values[0], type=self.type, id=id,
**attrs),
'<br/>',
tags.input(name=confirmname, value=values[0], type=self.type,
**attrs),
' ', tags.span(form.req._('confirm password'),
**{'class': 'emphasis'})]
return u'\n'.join(inputs)
class PasswordSingleInput(Input):
"""<input type='password'> without a confirmation field"""
type = 'password'
class FileInput(Input):
"""<input type='file'>"""
type = 'file'
def _render_attrs(self, form, field):
# ignore value which makes no sense here (XXX even on form validation error?)
name, values, attrs = super(FileInput, self)._render_attrs(form, field)
return name, ('',), attrs
class HiddenInput(Input):
"""<input type='hidden'>"""
type = 'hidden'
setdomid = False # by default, don't set id attribute on hidden input
settabindex = False
class ButtonInput(Input):
"""<input type='button'>
if you want a global form button, look at the Button, SubmitButton,
ResetButton and ImgButton classes below.
"""
type = 'button'
class TextArea(FieldWidget):
"""<textarea>"""
def render(self, form, field, renderer):
name, values, attrs = self._render_attrs(form, field)
attrs.setdefault('onkeyup', 'autogrow(this)')
if not values:
value = u''
elif len(values) == 1:
value = values[0]
else:
raise ValueError('a textarea is not supposed to be multivalued')
lines = value.splitlines()
linecount = len(lines)
for line in lines:
linecount += len(line) / 80
attrs.setdefault('cols', 80)
attrs.setdefault('rows', min(15, linecount + 2))
return tags.textarea(value, name=name, **attrs)
class FCKEditor(TextArea):
"""FCKEditor enabled <textarea>"""
def __init__(self, *args, **kwargs):
super(FCKEditor, self).__init__(*args, **kwargs)
self.attrs['cubicweb:type'] = 'wysiwyg'
def render(self, form, field, renderer):
form.req.fckeditor_config()
return super(FCKEditor, self).render(form, field, renderer)
class Select(FieldWidget):
"""<select>, for field having a specific vocabulary"""
vocabulary_widget = True
def __init__(self, attrs=None, multiple=False):
super(Select, self).__init__(attrs)
self._multiple = multiple
def render(self, form, field, renderer):
name, curvalues, attrs = self._render_attrs(form, field)
if not 'size' in attrs:
attrs['size'] = self._multiple and '5' or '1'
options = []
optgroup_opened = False
for option in field.vocabulary(form):
try:
label, value, oattrs = option
except ValueError:
label, value = option
oattrs = {}
if value is None:
# handle separator
if optgroup_opened:
options.append(u'</optgroup>')
oattrs.setdefault('label', label or '')
options.append(u'<optgroup %s>' % uilib.sgml_attributes(oattrs))
optgroup_opened = True
elif value in curvalues:
options.append(tags.option(label, value=value,
selected='selected', **oattrs))
else:
options.append(tags.option(label, value=value, **oattrs))
if optgroup_opened:
options.append(u'</optgroup>')
return tags.select(name=name, multiple=self._multiple,
options=options, **attrs)
class CheckBox(Input):
"""<input type='checkbox'>, for field having a specific vocabulary. One
input will be generated for each possible value.
"""
type = 'checkbox'
vocabulary_widget = True
def render(self, form, field, renderer):
name, curvalues, attrs = self._render_attrs(form, field)
domid = attrs.pop('id', None)
sep = attrs.pop('separator', u'<br/>\n')
options = []
for i, option in enumerate(field.vocabulary(form)):
try:
label, value, oattrs = option
except ValueError:
label, value = option
oattrs = {}
iattrs = attrs.copy()
iattrs.update(oattrs)
if i == 0 and domid is not None:
iattrs.setdefault('id', domid)
if value in curvalues:
iattrs['checked'] = u'checked'
tag = tags.input(name=name, type=self.type, value=value, **iattrs)
options.append(tag + label)
return sep.join(options)
class Radio(CheckBox):
"""<input type='radio'>, for field having a specific vocabulary. One
input will be generated for each possible value.
"""
type = 'radio'
# compound widgets #############################################################
class IntervalWidget(FieldWidget):
"""custom widget to display an interval composed by 2 fields. This widget
is expected to be used with a CompoundField containing the two actual
fields.
Exemple usage::
from uicfg import autoform_field, autoform_section
autoform_field.tag_attribute(('Concert', 'minprice'),
CompoundField(fields=(IntField(name='minprice'),
IntField(name='maxprice')),
label=_('price'),
widget=IntervalWidget()
))
# we've to hide the other field manually for now
autoform_section.tag_attribute(('Concert', 'maxprice'), 'generated')
"""
def render(self, form, field, renderer):
actual_fields = field.fields
assert len(actual_fields) == 2
return u'<div>%s %s %s %s</div>' % (
form.req._('from_interval_start'),
actual_fields[0].render(form, renderer),
form.req._('to_interval_end'),
actual_fields[1].render(form, renderer),
)
class HorizontalLayoutWidget(FieldWidget):
"""custom widget to display a set of fields grouped together horizontally
in a form. See `IntervalWidget` for example usage.
"""
def render(self, form, field, renderer):
if self.attrs.get('display_label', True):
subst = self.attrs.get('label_input_substitution', '%(label)s %(input)s')
fields = [subst % {'label': renderer.render_label(form, f),
'input': f.render(form, renderer)}
for f in field.subfields(form)]
else:
fields = [f.render(form, renderer) for f in field.subfields(form)]
return u'<div>%s</div>' % ' '.join(fields)
# javascript widgets ###########################################################
class DateTimePicker(TextInput):
"""<input type='text' + javascript date/time picker for date or datetime
fields
"""
monthnames = ('january', 'february', 'march', 'april',
'may', 'june', 'july', 'august',
'september', 'october', 'november', 'december')
daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday')
needs_js = ('cubicweb.calendar.js',)
needs_css = ('cubicweb.calendar_popup.css',)
@classmethod
def add_localized_infos(cls, req):
"""inserts JS variables defining localized months and days"""
# import here to avoid dependancy from cubicweb-common to simplejson
_ = req._
monthnames = [_(mname) for mname in cls.monthnames]
daynames = [_(dname) for dname in cls.daynames]
req.html_headers.define_var('MONTHNAMES', monthnames)
req.html_headers.define_var('DAYNAMES', daynames)
def render(self, form, field, renderer):
txtwidget = super(DateTimePicker, self).render(form, field, renderer)
self.add_localized_infos(form.req)
cal_button = self._render_calendar_popup(form, field)
return txtwidget + cal_button
def _render_calendar_popup(self, form, field):
value = form.form_field_value(field)
if not value:
value = date.today()
inputid = form.context[field]['id']
helperid = '%shelper' % inputid
year, month = value.year, value.month
return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
<img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
% (helperid, inputid, year, month,
form.req.external_resource('CALENDAR_ICON'),
form.req._('calendar'), helperid) )
# ajax widgets ################################################################
def init_ajax_attributes(attrs, wdgtype, loadtype=u'auto'):
try:
attrs['klass'] += u' widget'
except KeyError:
attrs['klass'] = u'widget'
attrs.setdefault('cubicweb:wdgtype', wdgtype)
attrs.setdefault('cubicweb:loadtype', loadtype)
class AjaxWidget(FieldWidget):
"""simple <div> based ajax widget"""
def __init__(self, wdgtype, inputid=None, **kwargs):
super(AjaxWidget, self).__init__(**kwargs)
init_ajax_attributes(self.attrs, wdgtype)
if inputid is not None:
self.attrs['cubicweb:inputid'] = inputid
def render(self, form, field, renderer):
self.add_media(form)
attrs = self._render_attrs(form, field)[-1]
return tags.div(**attrs)
class AutoCompletionWidget(TextInput):
"""ajax widget for StringField, proposing matching existing values as you
type.
"""
needs_js = ('cubicweb.widgets.js', 'jquery.autocomplete.js')
needs_css = ('jquery.autocomplete.css',)
wdgtype = 'SuggestField'
loadtype = 'auto'
def __init__(self, *args, **kwargs):
try:
self.autocomplete_initfunc = kwargs.pop('autocomplete_initfunc')
except KeyError:
warn('use autocomplete_initfunc argument of %s constructor '
'instead of relying on autocomplete_initfuncs dictionary on '
'the entity class' % self.__class__.__name__,
DeprecationWarning)
self.autocomplete_initfunc = None
super(AutoCompletionWidget, self).__init__(*args, **kwargs)
def _render_attrs(self, form, field):
name, values, attrs = super(AutoCompletionWidget, self)._render_attrs(form, field)
init_ajax_attributes(attrs, self.wdgtype, self.loadtype)
# XXX entity form specific
attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity, field)
if not values:
values = ('',)
return name, values, attrs
def _get_url(self, entity, field):
if self.autocomplete_initfunc is None:
# XXX for bw compat
fname = entity.autocomplete_initfuncs[field.name]
else:
fname = self.autocomplete_initfunc
return entity.req.build_url('json', fname=fname, mode='remote',
pageid=entity.req.pageid)
class StaticFileAutoCompletionWidget(AutoCompletionWidget):
"""XXX describe me"""
wdgtype = 'StaticFileSuggestField'
def _get_url(self, entity, field):
if self.autocomplete_initfunc is None:
# XXX for bw compat
fname = entity.autocomplete_initfuncs[field.name]
else:
fname = self.autocomplete_initfunc
return entity.req.datadir_url + fname
class RestrictedAutoCompletionWidget(AutoCompletionWidget):
"""XXX describe me"""
wdgtype = 'RestrictedSuggestField'
class AddComboBoxWidget(Select):
def _render_attrs(self, form, field):
name, values, attrs = super(AddComboBoxWidget, self)._render_attrs(form, field)
init_ajax_attributes(self.attrs, 'AddComboBox')
# XXX entity form specific
entity = form.edited_entity
attrs['cubicweb:etype_to'] = entity.e_schema
etype_from = entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]
attrs['cubicweb:etype_from'] = etype_from
return name, values, attrs
def render(self, form, field, renderer):
return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
<div id="newvalue">
<input type="text" id="newopt" />
<a href="javascript:noop()" id="add_newopt"> </a></div>
'''
# buttons ######################################################################
class Button(Input):
"""<input type='button'>, base class for global form buttons
note label is a msgid which will be translated at form generation time, you
should not give an already translated string.
"""
type = 'button'
def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
setdomid=None, settabindex=None,
name='', value='', onclick=None, cwaction=None):
super(Button, self).__init__(attrs, setdomid, settabindex)
if isinstance(label, tuple):
self.label = label[0]
self.icon = label[1]
else:
self.label = label
self.icon = None
self.name = name
self.value = ''
self.onclick = onclick
self.cwaction = cwaction
self.attrs.setdefault('klass', 'validateButton')
def render(self, form, field=None, renderer=None):
label = form.req._(self.label)
attrs = self.attrs.copy()
if self.cwaction:
assert self.onclick is None
attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
self.cwaction, self.label, form.domid)
elif self.onclick:
attrs['onclick'] = self.onclick
if self.name:
attrs['name'] = name
if self.setdomid:
attrs['id'] = self.name
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form.req.next_tabindex()
if self.icon:
img = tags.img(src=form.req.external_resource(self.icon), alt=self.icon)
else:
img = u''
return tags.button(img + xml_escape(label), escapecontent=False,
value=label, type=self.type, **attrs)
class SubmitButton(Button):
"""<input type='submit'>, main button to submit a form"""
type = 'submit'
class ResetButton(Button):
"""<input type='reset'>, main button to reset a form.
You usually don't want this.
"""
type = 'reset'
class ImgButton(object):
"""<img> wrapped into a <a> tag with href triggering something (usually a
javascript call)
note label is a msgid which will be translated at form generation time, you
should not give an already translated string.
"""
def __init__(self, domid, href, label, imgressource):
self.domid = domid
self.href = href
self.imgressource = imgressource
self.label = label
def render(self, form, field=None, renderer=None):
label = form.req._(self.label)
imgsrc = form.req.external_resource(self.imgressource)
return '<a id="%(domid)s" href="%(href)s">'\
'<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
'label': label, 'imgsrc': imgsrc,
'domid': self.domid, 'href': self.href}
# XXX EntityLinkComboBoxWidget, [Raw]DynamicComboBoxWidget