--- a/web/formwidgets.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1126 +0,0 @@
-# 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}