web/formwidgets.py
branchstable
changeset 5368 d321e4b62a10
parent 5367 4176a50c81c9
child 5377 84d14ddfae13
--- a/web/formwidgets.py	Wed Apr 21 16:53:25 2010 +0200
+++ b/web/formwidgets.py	Wed Apr 21 16:53:47 2010 +0200
@@ -1,9 +1,75 @@
-"""widget classes for form construction
+# organization: Logilab
+# copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+# contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+# license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+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.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
 
-:organization: Logilab
-:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+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
+
+.. kill or document AddComboBoxWidget
+.. 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.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"
 
@@ -19,14 +85,50 @@
 
 
 class FieldWidget(object):
-    """abstract widget class"""
-    # javascript / css files required by the widget
+    """The abstract base class for widgets.
+
+    **Attributes**
+
+    Here are standard attributes of a widget, that may be set on concret
+    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 concret
+    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 = ()
-    # automatically set id and tabindex attributes ?
     setdomid = True
     settabindex = True
-    # to ease usage as a sub-widgets (eg widget used by another widget)
     suffix = None
     # does this widget expect a vocabulary
     vocabulary_widget = False
@@ -51,12 +153,19 @@
         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.
 
-    def render(self, form, field, renderer=None):
+        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 concret widget classes.
+        """
         raise NotImplementedError()
 
     def format_value(self, form, field, value):
@@ -75,6 +184,30 @@
         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
+        concret classes.
+        """
         values = None
         if not field.ignore_req_params:
             qname = field.input_name(form, self.suffix)
@@ -112,6 +245,10 @@
         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, basestring):
@@ -152,13 +289,29 @@
 # basic html widgets ###########################################################
 
 class TextInput(Input):
-    """<input type='text'>"""
+    """Simple <input type='text'>, will return an unicode string."""
     type = 'text'
 
 
+class PasswordSingleInput(Input):
+    """Simple <input type='password'>, will return an 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 its confirmation field (using
-    <field's name>-confirm as name)
+    """<input type='password'> and a confirmation input. Form processing will
+    fail if password and confirmation differs, else it will return the password
+    as an utf-8 encoded string.
     """
     type = 'password'
 
@@ -186,19 +339,11 @@
         raise ProcessFormError(form._cw._("password and confirmation don't match"))
 
 
-class PasswordSingleInput(Input):
-    """<input type='password'> without a confirmation field"""
-    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 FileInput(Input):
-    """<input type='file'>"""
+    """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):
@@ -207,23 +352,25 @@
 
 
 class HiddenInput(Input):
-    """<input type='hidden'>"""
+    """Simple <input type='hidden'> for hidden value, will return an unicode
+    string.
+    """
     type = 'hidden'
     setdomid = False # by default, don't set id attribute on hidden input
     settabindex = False
 
 
 class ButtonInput(Input):
-    """<input type='button'>
+    """Simple <input type='button'>, will return an unicode string.
 
-    if you want a global form button, look at the Button, SubmitButton,
-    ResetButton and ImgButton classes below.
+    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):
-    """<textarea>"""
+    """Simple <textarea>, will return an unicode string."""
 
     def _render(self, form, field, renderer):
         values, attrs = self.values_and_attributes(form, field)
@@ -245,7 +392,9 @@
 
 
 class FCKEditor(TextArea):
-    """FCKEditor enabled <textarea>"""
+    """FCKEditor enabled <textarea>, will return an unicode string containing
+    HTML formated text.
+    """
     def __init__(self, *args, **kwargs):
         super(FCKEditor, self).__init__(*args, **kwargs)
         self.attrs['cubicweb:type'] = 'wysiwyg'
@@ -256,7 +405,9 @@
 
 
 class Select(FieldWidget):
-    """<select>, for field having a specific vocabulary"""
+    """Simple <select>, for field having a specific vocabulary. Will return
+    an unicode string, or a list of unicode strings.
+    """
     vocabulary_widget = True
 
     def __init__(self, attrs=None, multiple=False, **kwargs):
@@ -294,8 +445,11 @@
 
 
 class CheckBox(Input):
-    """<input type='checkbox'>, for field having a specific vocabulary. One
-    input will be generated for each possible value.
+    """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'
     vocabulary_widget = True
@@ -334,8 +488,11 @@
 
 
 class Radio(CheckBox):
-    """<input type='radio'>, for field having a specific vocabulary. One
+    """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'
     
@@ -343,8 +500,8 @@
 # javascript widgets ###########################################################
 
 class DateTimePicker(TextInput):
-    """<input type='text' + javascript date/time picker for date or datetime
-    fields
+    """<input type='text'> + javascript date/time picker for date or datetime
+    fields. Will return the date or datetime as an unicode string.
     """
     monthnames = ('january', 'february', 'march', 'april',
                   'may', 'june', 'july', 'august',
@@ -386,7 +543,9 @@
 
 
 class JQueryDatePicker(FieldWidget):
-    """use jquery.ui.datepicker to define a date time picker"""
+    """Use jquery.ui.datepicker to define a date picker. Will return the date as
+    an unicode string.
+    """
     needs_js = ('jquery.ui.js', )
     needs_css = ('jquery.ui.css',)
 
@@ -413,7 +572,9 @@
 
 
 class JQueryTimePicker(FieldWidget):
-    """use jquery.timePicker.js to define a js time picker"""
+    """Use jquery.timePicker to define a time picker. Will return the time as an
+    unicode string.
+    """
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
 
@@ -437,6 +598,10 @@
 
 
 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
@@ -496,7 +661,9 @@
 
 
 class AjaxWidget(FieldWidget):
-    """simple <div> based ajax widget"""
+    """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)
@@ -509,8 +676,10 @@
 
 
 class AutoCompletionWidget(TextInput):
-    """ajax widget for StringField, proposing matching existing values as you
-    type.
+    """<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.autocomplete.js')
     needs_css = ('jquery.autocomplete.css',)
@@ -616,10 +785,10 @@
 # buttons ######################################################################
 
 class Button(Input):
-    """<input type='button'>, base class for global form buttons
+    """Simple <input type='button'>, base class for global form buttons.
 
-    note label is a msgid which will be translated at form generation time, you
-    should not give an already translated string.
+    Note that `label` is a msgid which will be translated at form generation
+    time, you should not give an already translated string.
     """
     type = 'button'
     def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
@@ -662,23 +831,20 @@
 
 
 class SubmitButton(Button):
-    """<input type='submit'>, main button to submit a form"""
+    """Simple <input type='submit'>, main button to submit a form"""
     type = 'submit'
 
 
 class ResetButton(Button):
-    """<input type='reset'>, main button to reset a form.
-    You usually don't want this.
+    """Simple <input type='reset'>, main button to reset a form. You usually
+    don't want to use this.
     """
     type = 'reset'
 
 
 class ImgButton(object):
-    """<img> wrapped into a <a> tag with href triggering something (usually a
-    javascript call)
-
-    note label is a msgid which will be translated at form generation time, you
-    should not give an already translated string.
+    """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
@@ -697,17 +863,53 @@
 
 # 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 an url path / query string (used by
-    default for Bookmark.path for instance), dealing with url quoting nicely
-    (eg user edit the unquoted value).
+    """Custom widget to edit separatly an 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):
-        """render the widget for the given `field` of `form`.
-
-        Generate one <input> tag for each field's value
-        """
         assert self.suffix is None, 'not supported'
         req = form._cw
         pathqname = field.input_name(form, 'path')