[views] improve JQueryDatePicker so that a date field can be defined as the min/max of an other one
closes #5169039
# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""
Widgets
~~~~~~~
.. Note::
A widget is responsible for the display of a field. It may use more than one
HTML input tags. When the form is posted, a widget is also reponsible to give
back to the field something it can understand.
Of course you can not use any widget with any field...
.. autoclass:: cubicweb.web.formwidgets.FieldWidget
HTML <input> based widgets
''''''''''''''''''''''''''
.. autoclass:: cubicweb.web.formwidgets.HiddenInput
.. autoclass:: cubicweb.web.formwidgets.TextInput
.. autoclass:: cubicweb.web.formwidgets.EmailInput
.. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput
.. autoclass:: cubicweb.web.formwidgets.FileInput
.. autoclass:: cubicweb.web.formwidgets.ButtonInput
Other standard HTML widgets
'''''''''''''''''''''''''''
.. autoclass:: cubicweb.web.formwidgets.TextArea
.. autoclass:: cubicweb.web.formwidgets.Select
.. autoclass:: cubicweb.web.formwidgets.CheckBox
.. autoclass:: cubicweb.web.formwidgets.Radio
Date and time widgets
'''''''''''''''''''''
.. autoclass:: cubicweb.web.formwidgets.DateTimePicker
.. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker
.. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker
.. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker
Ajax / javascript widgets
'''''''''''''''''''''''''
.. autoclass:: cubicweb.web.formwidgets.FCKEditor
.. autoclass:: cubicweb.web.formwidgets.AjaxWidget
.. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
.. autoclass:: cubicweb.web.formwidgets.InOutWidget
.. kill or document StaticFileAutoCompletionWidget
.. kill or document LazyRestrictedAutoCompletionWidget
.. kill or document RestrictedAutoCompletionWidget
Other widgets
'''''''''''''
.. autoclass:: cubicweb.web.formwidgets.PasswordInput
.. autoclass:: cubicweb.web.formwidgets.IntervalWidget
.. autoclass:: cubicweb.web.formwidgets.BitSelect
.. autoclass:: cubicweb.web.formwidgets.HorizontalLayoutWidget
.. autoclass:: cubicweb.web.formwidgets.EditableURLWidget
Form controls
'''''''''''''
Those classes are not proper widget (they are not associated to field) but are
used as form controls. Their API is similar to widgets except that `field`
argument given to :meth:`render` will be `None`.
.. autoclass:: cubicweb.web.formwidgets.Button
.. autoclass:: cubicweb.web.formwidgets.SubmitButton
.. autoclass:: cubicweb.web.formwidgets.ResetButton
.. autoclass:: cubicweb.web.formwidgets.ImgButton
"""
__docformat__ = "restructuredtext en"
from functools import reduce
from datetime import date
from warnings import warn
from six import text_type, string_types
from logilab.mtconverter import xml_escape
from logilab.common.deprecation import deprecated
from logilab.common.date import todatetime
from cubicweb import tags, uilib
from cubicweb.utils import json_dumps
from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE, ProcessFormError
class FieldWidget(object):
"""The abstract base class for widgets.
**Attributes**
Here are standard attributes of a widget, that may be set on concrete class
to override default behaviours:
:attr:`needs_js`
list of javascript files needed by the widget.
:attr:`needs_css`
list of css files needed by the widget.
:attr:`setdomid`
flag telling if HTML DOM identifier should be set on input.
:attr:`settabindex`
flag telling if HTML tabindex attribute of inputs should be set.
:attr:`suffix`
string to use a suffix when generating input, to ease usage as a
sub-widgets (eg widget used by another widget)
:attr:`vocabulary_widget`
flag telling if this widget expect a vocabulary
Also, widget instances takes as first argument a `attrs` dictionary which
will be stored in the attribute of the same name. It contains HTML
attributes that should be set in the widget's input tag (though concrete
classes may ignore it).
.. currentmodule:: cubicweb.web.formwidgets
**Form generation methods**
.. automethod:: render
.. automethod:: _render
.. automethod:: values
.. automethod:: attributes
**Post handling methods**
.. automethod:: process_field_data
"""
needs_js = ()
needs_css = ()
setdomid = True
settabindex = True
suffix = None
# does this widget expect a vocabulary
vocabulary_widget = False
def __init__(self, attrs=None, setdomid=None, settabindex=None, suffix=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
if suffix is not None:
self.suffix = suffix
def add_media(self, form):
"""adds media (CSS & JS) required by this widget"""
if self.needs_js:
form._cw.add_js(self.needs_js)
if self.needs_css:
form._cw.add_css(self.needs_css)
def render(self, form, field, renderer=None):
"""Called to render the widget for the given `field` in the given
`form`. Return a unicode string containing the HTML snippet.
You will usually prefer to override the :meth:`_render` method so you
don't have to handle addition of needed javascript / css files.
"""
self.add_media(form)
return self._render(form, field, renderer)
def _render(self, form, field, renderer):
"""This is the method you have to implement in concrete widget classes.
"""
raise NotImplementedError()
def format_value(self, form, field, value):
return field.format_value(form._cw, value)
def attributes(self, form, field):
"""Return HTML attributes for the widget, automatically setting DOM
identifier and tabindex when desired (see :attr:`setdomid` and
:attr:`settabindex` attributes)
"""
attrs = dict(self.attrs)
if self.setdomid:
attrs['id'] = field.dom_id(form, self.suffix)
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form._cw.next_tabindex()
if 'placeholder' in attrs:
attrs['placeholder'] = form._cw._(attrs['placeholder'])
return attrs
def values(self, form, field):
"""Return the current *string* values (i.e. for display in an HTML
string) for the given field. This method returns a list of values since
it's suitable for all kind of widgets, some of them taking multiple
values, but you'll get a single value in the list in most cases.
Those values are searched in:
1. previously submitted form values if any (on validation error)
2. req.form (specified using request parameters)
3. extra form values given to form.render call (specified the code
generating the form)
4. field's typed value (returned by its
:meth:`~cubicweb.web.formfields.Field.typed_value` method)
Values found in 1. and 2. are expected te be already some 'display
value' (eg a string) while those found in 3. and 4. are expected to be
correctly typed value.
3 and 4 are handle by the :meth:`typed_value` method to ease reuse in
concrete classes.
"""
values = None
if not field.ignore_req_params:
qname = field.input_name(form, self.suffix)
# value from a previous post that has raised a validation error
if qname in form.form_previous_values:
values = form.form_previous_values[qname]
# value specified using form parameters
elif qname in form._cw.form:
values = form._cw.form[qname]
elif field.name != qname and field.name in form._cw.form:
# XXX compat: accept attr=value in req.form to specify value of
# attr-subject
values = form._cw.form[field.name]
if values is None:
values = self.typed_value(form, field)
if values != INTERNAL_FIELD_VALUE:
values = self.format_value(form, field, values)
if not isinstance(values, (tuple, list)):
values = (values,)
return values
def typed_value(self, form, field):
"""return field's *typed* value specified in:
3. extra form values given to render()
4. field's typed value
"""
qname = field.input_name(form)
for key in ((field, form), qname):
try:
return form.formvalues[key]
except KeyError:
continue
if field.name != qname and field.name in form.formvalues:
return form.formvalues[field.name]
return field.typed_value(form)
def process_field_data(self, form, field):
"""Return process posted value(s) for widget and return something
understandable by the associated `field`. That value may be correctly
typed or a string that the field may parse.
"""
posted = form._cw.form
val = posted.get(field.input_name(form, self.suffix))
if isinstance(val, string_types):
val = val.strip()
return val
# XXX deprecates
def values_and_attributes(self, form, field):
return self.values(form, field), self.attributes(form, field)
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
"""
values, attrs = self.values_and_attributes(form, field)
# ensure something is rendered
if not values:
values = (INTERNAL_FIELD_VALUE,)
inputs = [tags.input(name=field.input_name(form, self.suffix),
type=self.type, value=value, **attrs)
for value in values]
return u'\n'.join(inputs)
# basic html widgets ###########################################################
class TextInput(Input):
"""Simple <input type='text'>, will return a unicode string."""
type = 'text'
class EmailInput(Input):
"""Simple <input type='email'>, will return a unicode string."""
type = 'email'
class PasswordSingleInput(Input):
"""Simple <input type='password'>, will return a utf-8 encoded string.
You may prefer using the :class:`~cubicweb.web.formwidgets.PasswordInput`
widget which handles password confirmation.
"""
type = 'password'
def process_field_data(self, form, field):
value = super(PasswordSingleInput, self).process_field_data(form, field)
if value is not None:
return value.encode('utf-8')
return value
class PasswordInput(Input):
"""<input type='password'> and a confirmation input. Form processing will
fail if password and confirmation differs, else it will return the password
as a utf-8 encoded string.
"""
type = 'password'
def _render(self, form, field, renderer):
assert self.suffix is None, 'suffix not supported'
values, attrs = self.values_and_attributes(form, field)
assert len(values) == 1
domid = attrs.pop('id')
inputs = [tags.input(name=field.input_name(form),
value=values[0], type=self.type, id=domid, **attrs),
'<br/>',
tags.input(name=field.input_name(form, '-confirm'),
value=values[0], type=self.type, **attrs),
' ', tags.span(form._cw._('confirm password'),
**{'class': 'emphasis'})]
return u'\n'.join(inputs)
def process_field_data(self, form, field):
passwd1 = super(PasswordInput, self).process_field_data(form, field)
passwd2 = form._cw.form.get(field.input_name(form, '-confirm'))
if passwd1 == passwd2:
if passwd1 is None:
return None
return passwd1.encode('utf-8')
raise ProcessFormError(form._cw._("password and confirmation don't match"))
class FileInput(Input):
"""Simple <input type='file'>, will return a tuple (name, stream) where
name is the posted file name and stream a file like object containing the
posted file data.
"""
type = 'file'
def values(self, form, field):
# ignore value which makes no sense here (XXX even on form validation error?)
return ('',)
class HiddenInput(Input):
"""Simple <input type='hidden'> for hidden value, will return a unicode
string.
"""
type = 'hidden'
setdomid = False # by default, don't set id attribute on hidden input
settabindex = False
class ButtonInput(Input):
"""Simple <input type='button'>, will return a unicode string.
If you want a global form button, look at the :class:`Button`,
:class:`SubmitButton`, :class:`ResetButton` and :class:`ImgButton` below.
"""
type = 'button'
class TextArea(FieldWidget):
"""Simple <textarea>, will return a unicode string."""
_minrows = 2
_maxrows = 15
_columns = 80
def _render(self, form, field, renderer):
values, attrs = self.values_and_attributes(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) // self._columns
attrs.setdefault('cols', self._columns)
attrs.setdefault('rows', min(self._maxrows, linecount + self._minrows))
return tags.textarea(value, name=field.input_name(form, self.suffix),
**attrs)
class FCKEditor(TextArea):
"""FCKEditor enabled <textarea>, will return a unicode string containing
HTML formated text.
"""
def __init__(self, *args, **kwargs):
super(FCKEditor, self).__init__(*args, **kwargs)
self.attrs['cubicweb:type'] = 'wysiwyg'
def _render(self, form, field, renderer):
form._cw.fckeditor_config()
return super(FCKEditor, self)._render(form, field, renderer)
class Select(FieldWidget):
"""Simple <select>, for field having a specific vocabulary. Will return
a unicode string, or a list of unicode strings.
"""
vocabulary_widget = True
default_size = 10
def __init__(self, attrs=None, multiple=False, **kwargs):
super(Select, self).__init__(attrs, **kwargs)
self._multiple = multiple
def _render(self, form, field, renderer):
curvalues, attrs = self.values_and_attributes(form, field)
options = []
optgroup_opened = False
vocab = field.vocabulary(form)
for option in vocab:
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 self.value_selected(value, 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>')
if not 'size' in attrs:
if self._multiple:
size = text_type(min(self.default_size, len(vocab) or 1))
else:
size = u'1'
attrs['size'] = size
return tags.select(name=field.input_name(form, self.suffix),
multiple=self._multiple, options=options, **attrs)
def value_selected(self, value, curvalues):
return value in curvalues
class InOutWidget(Select):
needs_js = ('cubicweb.widgets.js', )
default_size = 10
template = """
<table id="%(widgetid)s">
<tr>
<td>%(inoutinput)s</td>
<td><div style="margin-bottom:3px">%(addinput)s</div>
<div>%(removeinput)s</div>
</td>
<td>%(resinput)s</td>
</tr>
</table>
"""
add_button = ('<input type="button" class="wdgButton cwinoutadd" '
'value=">>" size="10" />')
remove_button = ('<input type="button" class="wdgButton cwinoutremove" '
'value="<<" size="10" />')
def __init__(self, *args, **kwargs):
super(InOutWidget, self).__init__(*args, **kwargs)
self._multiple = True
def render_select(self, form, field, name, selected=False):
values, attrs = self.values_and_attributes(form, field)
options = []
inputs = []
for option in field.vocabulary(form):
try:
label, value, _oattrs = option
except ValueError:
label, value = option
if selected:
# add values
if value in values:
options.append(tags.option(label, value=value))
# add hidden inputs
inputs.append(tags.input(value=value,
name=field.dom_id(form),
type="hidden"))
else:
if value not in values:
options.append(tags.option(label, value=value))
if 'size' not in attrs:
attrs['size'] = self.default_size
if 'id' in attrs :
attrs.pop('id')
return tags.select(name=name, multiple=self._multiple, id=name,
options=options, **attrs) + '\n'.join(inputs)
def _render(self, form, field, renderer):
domid = field.dom_id(form)
jsnodes = {'widgetid': domid,
'from': 'from_' + domid,
'to': 'to_' + domid}
form._cw.add_onload(u'$(cw.jqNode("%s")).cwinoutwidget("%s", "%s");'
% (jsnodes['widgetid'], jsnodes['from'], jsnodes['to']))
field.required = True
return (self.template %
{'widgetid': jsnodes['widgetid'],
# helpinfo select tag
'inoutinput' : self.render_select(form, field, jsnodes['from']),
# select tag with resultats
'resinput' : self.render_select(form, field, jsnodes['to'], selected=True),
'addinput' : self.add_button % jsnodes,
'removeinput': self.remove_button % jsnodes
})
class BitSelect(Select):
"""Select widget for IntField using a vocabulary with bit masks as values.
See also :class:`~cubicweb.web.facet.BitFieldFacet`.
"""
def __init__(self, attrs=None, multiple=True, **kwargs):
super(BitSelect, self).__init__(attrs, multiple=multiple, **kwargs)
def value_selected(self, value, curvalues):
mask = reduce(lambda x, y: int(x) | int(y), curvalues, 0)
return int(value) & mask
def process_field_data(self, form, field):
"""Return process posted value(s) for widget and return something
understandable by the associated `field`. That value may be correctly
typed or a string that the field may parse.
"""
val = super(BitSelect, self).process_field_data(form, field)
if isinstance(val, list):
val = reduce(lambda x, y: int(x) | int(y), val, 0)
elif val:
val = int(val)
else:
val = 0
return val
class CheckBox(Input):
"""Simple <input type='checkbox'>, for field having a specific
vocabulary. One input will be generated for each possible value.
You can specify separator using the `separator` constructor argument, by
default <br/> is used.
"""
type = 'checkbox'
default_separator = u'<br/>\n'
vocabulary_widget = True
def __init__(self, attrs=None, separator=None, **kwargs):
super(CheckBox, self).__init__(attrs, **kwargs)
self.separator = separator or self.default_separator
def _render(self, form, field, renderer):
curvalues, attrs = self.values_and_attributes(form, field)
domid = attrs.pop('id', None)
sep = self.separator
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=field.input_name(form, self.suffix),
type=self.type, value=value, **iattrs)
options.append(u'<label>%s %s</label>' % (tag, xml_escape(label)))
return sep.join(options)
class Radio(CheckBox):
"""Simle <input type='radio'>, for field having a specific vocabulary. One
input will be generated for each possible value.
You can specify separator using the `separator` constructor argument, by
default <br/> is used.
"""
type = 'radio'
# javascript widgets ###########################################################
class DateTimePicker(TextInput):
"""<input type='text'> + javascript date/time picker for date or datetime
fields. Will return the date or datetime as a unicode string.
"""
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"""
_ = 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._cw)
cal_button = self._render_calendar_popup(form, field)
return txtwidget + cal_button
def _render_calendar_popup(self, form, field):
value = field.typed_value(form)
if not value:
value = date.today()
inputid = field.dom_id(form)
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._cw.uiprops['CALENDAR_ICON'],
form._cw._('calendar'), helperid) )
class JQueryDatePicker(FieldWidget):
"""Use jquery.ui.datepicker to define a date picker. Will return the date as
a unicode string.
You can couple DatePickers by using the min_of and/or max_of parameters.
The DatePicker identified by the value of min_of(/max_of) will force the user to
choose a date anterior(/posterior) to this DatePicker.
example:
start and end are two JQueryDatePicker and start must always be before end
affk.set_field_kwargs(etype, 'start_date', widget=JQueryDatePicker(min_of='end_date'))
affk.set_field_kwargs(etype, 'end_date', widget=JQueryDatePicker(max_of='start_date'))
That way, on change of end(/start) value a new max(/min) will be set for start(/end)
The invalid dates will be gray colored in the datepicker
"""
needs_js = ('jquery.ui.js', )
needs_css = ('jquery.ui.css',)
default_size = 10
def __init__(self, datestr=None, min_of=None, max_of=None, **kwargs):
super(JQueryDatePicker, self).__init__(**kwargs)
self.min_of = min_of
self.max_of = max_of
self.value = datestr
def attributes(self, form, field):
form._cw.add_js('cubicweb.widgets.js')
attrs = super(JQueryDatePicker, self).attributes(form, field)
if self.max_of:
attrs['data-max-of'] = '%s-subject:%s' % (self.max_of, form.edited_entity.eid)
if self.min_of:
attrs['data-min-of'] = '%s-subject:%s' % (self.min_of, form.edited_entity.eid)
return attrs
def _render(self, form, field, renderer):
req = form._cw
if req.lang != 'en':
req.add_js('jquery.ui.datepicker-%s.js' % req.lang)
domid = field.dom_id(form, self.suffix)
# XXX find a way to understand every format
fmt = req.property_value('ui.date-format')
picker_fmt = fmt.replace('%Y', 'yy').replace('%m', 'mm').replace('%d', 'dd')
max_date = min_date = None
if self.min_of:
current = getattr(form.edited_entity, self.min_of)
if current is not None:
max_date = current.strftime(fmt)
if self.max_of:
current = getattr(form.edited_entity, self.max_of)
if current is not None:
min_date = current.strftime(fmt)
req.add_onload(u'renderJQueryDatePicker("%s", "%s", "%s", %s, %s);'
% (domid, req.uiprops['CALENDAR_ICON'], picker_fmt, json_dumps(min_date),
json_dumps(max_date)))
return self._render_input(form, field)
def _render_input(self, form, field):
if self.value is None:
value = self.values(form, field)[0]
else:
value = self.value
attrs = self.attributes(form, field)
attrs.setdefault('size', text_type(self.default_size))
return tags.input(name=field.input_name(form, self.suffix),
value=value, type='text', **attrs)
class JQueryTimePicker(JQueryDatePicker):
"""Use jquery.timePicker to define a time picker. Will return the time as a
unicode string.
"""
needs_js = ('jquery.timePicker.js',)
needs_css = ('jquery.timepicker.css',)
default_size = 5
def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
super(JQueryTimePicker, self).__init__(timestr, **kwargs)
self.timesteps = timesteps
self.separator = separator
def _render(self, form, field, renderer):
domid = field.dom_id(form, self.suffix)
form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
domid, self.timesteps, self.separator))
return self._render_input(form, field)
class JQueryDateTimePicker(FieldWidget):
"""Compound widget using :class:`JQueryDatePicker` and
:class:`JQueryTimePicker` widgets to define a date and time picker. Will
return the date and time as python datetime instance.
"""
def __init__(self, initialtime=None, timesteps=15, **kwargs):
super(JQueryDateTimePicker, self).__init__(**kwargs)
self.initialtime = initialtime
self.timesteps = timesteps
def _render(self, form, field, renderer):
"""render the widget for the given `field` of `form`.
Generate one <input> tag for each field's value
"""
req = form._cw
dateqname = field.input_name(form, 'date')
timeqname = field.input_name(form, 'time')
if dateqname in form.form_previous_values:
datestr = form.form_previous_values[dateqname]
timestr = form.form_previous_values[timeqname]
else:
datestr = timestr = u''
if field.name in req.form:
value = req.parse_datetime(req.form[field.name])
else:
value = self.typed_value(form, field)
if value:
datestr = req.format_date(value)
timestr = req.format_time(value)
elif self.initialtime:
timestr = req.format_time(self.initialtime)
datepicker = JQueryDatePicker(datestr=datestr, suffix='date')
timepicker = JQueryTimePicker(timestr=timestr, timesteps=self.timesteps,
suffix='time')
return u'<div id="%s">%s%s</div>' % (field.dom_id(form),
datepicker.render(form, field, renderer),
timepicker.render(form, field, renderer))
def process_field_data(self, form, field):
req = form._cw
datestr = req.form.get(field.input_name(form, 'date')).strip() or None
timestr = req.form.get(field.input_name(form, 'time')).strip() or None
if datestr is None:
return None
try:
date = todatetime(req.parse_datetime(datestr, 'Date'))
except ValueError as exc:
raise ProcessFormError(text_type(exc))
if timestr is None:
return date
try:
time = req.parse_datetime(timestr, 'Time')
except ValueError as exc:
raise ProcessFormError(text_type(exc))
return date.replace(hour=time.hour, minute=time.minute, second=time.second)
# ajax widgets ################################################################
def init_ajax_attributes(attrs, wdgtype, loadtype=u'auto'):
try:
attrs['class'] += u' widget'
except KeyError:
attrs['class'] = u'widget'
attrs.setdefault('cubicweb:wdgtype', wdgtype)
attrs.setdefault('cubicweb:loadtype', loadtype)
class AjaxWidget(FieldWidget):
"""Simple <div> based ajax widget, requiring a `wdgtype` argument telling
which javascript widget should be used.
"""
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):
attrs = self.values_and_attributes(form, field)[-1]
return tags.div(**attrs)
class AutoCompletionWidget(TextInput):
"""<input type='text'> based ajax widget, taking a `autocomplete_initfunc`
argument which should specify the name of a method of the json
controller. This method is expected to return allowed values for the input,
that the widget will use to propose matching values as you type.
"""
needs_js = ('cubicweb.widgets.js', 'jquery.ui.js')
needs_css = ('jquery.ui.css',)
default_settings = {}
def __init__(self, *args, **kwargs):
self.autocomplete_settings = kwargs.pop('autocomplete_settings',
self.default_settings)
self.autocomplete_initfunc = kwargs.pop('autocomplete_initfunc')
super(AutoCompletionWidget, self).__init__(*args, **kwargs)
def values(self, form, field):
values = super(AutoCompletionWidget, self).values(form, field)
if not values:
values = ('',)
return values
def _render(self, form, field, renderer):
entity = form.edited_entity
domid = field.dom_id(form).replace(':', r'\\:')
if callable(self.autocomplete_initfunc):
data = self.autocomplete_initfunc(form, field)
else:
data = xml_escape(self._get_url(entity, field))
form._cw.add_onload(u'$("#%s").cwautocomplete(%s, %s);'
% (domid, json_dumps(data),
json_dumps(self.autocomplete_settings)))
return super(AutoCompletionWidget, self)._render(form, field, renderer)
def _get_url(self, entity, field):
fname = self.autocomplete_initfunc
return entity._cw.build_url('ajax', fname=fname, mode='remote',
pageid=entity._cw.pageid)
class StaticFileAutoCompletionWidget(AutoCompletionWidget):
"""XXX describe me"""
wdgtype = 'StaticFileSuggestField'
def _get_url(self, entity, field):
return entity._cw.data_url(self.autocomplete_initfunc)
class RestrictedAutoCompletionWidget(AutoCompletionWidget):
"""XXX describe me"""
default_settings = {'mustMatch': True}
class LazyRestrictedAutoCompletionWidget(RestrictedAutoCompletionWidget):
"""remote autocomplete """
def values_and_attributes(self, form, field):
"""override values_and_attributes to handle initial displayed values"""
values, attrs = super(LazyRestrictedAutoCompletionWidget, self).values_and_attributes(form, field)
assert len(values) == 1, "multiple selection is not supported yet by LazyWidget"
if not values[0]:
values = form.cw_extra_kwargs.get(field.name,'')
if not isinstance(values, (tuple, list)):
values = (values,)
try:
values = list(values)
values[0] = int(values[0])
attrs['cubicweb:initialvalue'] = values[0]
values = (self.display_value_for(form, values[0]),)
except (TypeError, ValueError):
pass
return values, attrs
def display_value_for(self, form, value):
entity = form._cw.entity_from_eid(value)
return entity.view('combobox')
# more widgets #################################################################
class IntervalWidget(FieldWidget):
"""Custom widget to display an interval composed by 2 fields. This widget is
expected to be used with a :class:`CompoundField` containing the two actual
fields.
Exemple usage::
class MyForm(FieldsForm):
price = CompoundField(fields=(IntField(name='minprice'),
IntField(name='maxprice')),
label=_('price'),
widget=IntervalWidget())
"""
def _render(self, form, field, renderer):
actual_fields = field.fields
assert len(actual_fields) == 2
return u'<div>%s %s %s %s</div>' % (
form._cw._('from_interval_start'),
actual_fields[0].render(form, renderer),
form._cw._('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)
class EditableURLWidget(FieldWidget):
"""Custom widget to edit separatly a URL path / query string (used by
default for the `path` attribute of `Bookmark` entities).
It deals with url quoting nicely so that the user edit the unquoted value.
"""
def _render(self, form, field, renderer):
assert self.suffix is None, 'not supported'
req = form._cw
pathqname = field.input_name(form, 'path')
fqsqname = field.input_name(form, 'fqs') # formatted query string
if pathqname in form.form_previous_values:
path = form.form_previous_values[pathqname]
fqs = form.form_previous_values[fqsqname]
else:
if field.name in req.form:
value = req.form[field.name]
else:
value = self.typed_value(form, field)
if value:
try:
path, qs = value.split('?', 1)
except ValueError:
path = value
qs = ''
else:
path = qs = ''
fqs = u'\n'.join(u'%s=%s' % (k, v) for k, v in req.url_parse_qsl(qs))
attrs = dict(self.attrs)
if self.setdomid:
attrs['id'] = field.dom_id(form)
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = req.next_tabindex()
# ensure something is rendered
inputs = [u'<table><tr><th>',
req._('i18n_bookmark_url_path'),
u'</th><td>',
tags.input(name=pathqname, type='string', value=path, **attrs),
u'</td></tr><tr><th>',
req._('i18n_bookmark_url_fqs'),
u'</th><td>']
if self.setdomid:
attrs['id'] = field.dom_id(form, 'fqs')
if self.settabindex:
attrs['tabindex'] = req.next_tabindex()
attrs.setdefault('cols', 60)
attrs.setdefault('onkeyup', 'autogrow(this)')
inputs += [tags.textarea(fqs, name=fqsqname, **attrs),
u'</td></tr></table>']
# surrounding div necessary for proper error localization
return u'<div id="%s">%s</div>' % (
field.dom_id(form), u'\n'.join(inputs))
def process_field_data(self, form, field):
req = form._cw
values = {}
path = req.form.get(field.input_name(form, 'path'))
if isinstance(path, string_types):
path = path.strip()
if path is None:
path = u''
fqs = req.form.get(field.input_name(form, 'fqs'))
if isinstance(fqs, string_types):
fqs = fqs.strip() or None
if fqs:
for i, line in enumerate(fqs.split('\n')):
line = line.strip()
if line:
try:
key, val = line.split('=', 1)
except ValueError:
raise ProcessFormError(req._("wrong query parameter line %s") % (i+1))
# value will be url quoted by build_url_params
values.setdefault(key, []).append(val)
if not values:
return path
return u'%s?%s' % (path, req.build_url_params(**values))
# form controls ######################################################################
class Button(Input):
"""Simple <input type='button'>, base class for global form buttons.
Note that `label` is a msgid which will be translated at form generation
time, you should not give an already translated string.
"""
type = 'button'
css_class = 'validateButton'
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
def render(self, form, field=None, renderer=None):
label = form._cw._(self.label)
attrs = self.attrs.copy()
attrs.setdefault('class', self.css_class)
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'] = self.name
if self.setdomid:
attrs['id'] = self.name
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form._cw.next_tabindex()
if self.icon:
img = tags.img(src=form._cw.uiprops[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):
"""Simple <input type='submit'>, main button to submit a form"""
type = 'submit'
class ResetButton(Button):
"""Simple <input type='reset'>, main button to reset a form. You usually
don't want to use this.
"""
type = 'reset'
class ImgButton(object):
"""Simple <img> wrapped into a <a> tag with href triggering something (usually a
javascript call).
"""
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._cw._(self.label)
imgsrc = form._cw.uiprops[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}