--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/formwidgets.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1126 @@
+# 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 six import text_type, string_types
+
+from logilab.mtconverter import xml_escape
+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 'tabindex' not 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 'size' not 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 'tabindex' not 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:
+ msg = req._("wrong query parameter line %s") % (i + 1)
+ raise ProcessFormError(msg)
+ # 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 'tabindex' not 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}