diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/formwidgets.py --- /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 . +""" +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 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 tag based widgets""" + type = None + + def _render(self, form, field, renderer): + """render the widget for the given `field` of `form`. + + Generate one 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 , will return a unicode string.""" + type = 'text' + + +class EmailInput(Input): + """Simple , will return a unicode string.""" + type = 'email' + + +class PasswordSingleInput(Input): + """Simple , 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): + """ 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), + '
', + 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 , 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 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 , 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