web/formfields.py
changeset 5385 b6e250dd7a7d
parent 5270 6297d5265572
parent 5368 d321e4b62a10
child 5423 e15abfdcce38
--- a/web/formfields.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/formfields.py	Fri Apr 23 12:42:53 2010 +0200
@@ -1,11 +1,50 @@
-"""Fields are used to control what's displayed in forms. It makes the link
-between something to edit and its display in the form. Actual display is handled
-by a widget associated to the field.
+# 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
+"""
+The Field class and basic fields
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. Note::
+  Fields are used to control what's edited in forms. They makes the link between
+  something to edit and its display in the form. Actual display is handled by a
+  widget associated to the field.
+
+Let first see the base class for fields:
+
+.. autoclass:: cubicweb.web.formfields.Field
+
+Now, you usually don't use that class but one of the concret field classes
+described below, according to what you want to edit.
+
+Basic fields
+''''''''''''
 
-: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
+.. autoclass:: cubicweb.web.formfields.StringField()
+.. autoclass:: cubicweb.web.formfields.PasswordField()
+.. autoclass:: cubicweb.web.formfields.IntField()
+.. autoclass:: cubicweb.web.formfields.FloatField()
+.. autoclass:: cubicweb.web.formfields.BooleanField()
+.. autoclass:: cubicweb.web.formfields.DateField()
+.. autoclass:: cubicweb.web.formfields.DateTimeField()
+.. autoclass:: cubicweb.web.formfields.TimeField()
+
+Compound fields
+''''''''''''''''
+
+.. autoclass:: cubicweb.web.formfields.RichTextField()
+.. autoclass:: cubicweb.web.formfields.FileField()
+.. autoclass:: cubicweb.web.formfields.CompoundField()
+
+.. autoclass cubicweb.web.formfields.EditableFileField() XXX should be a widget
+
+Entity specific fields and function
+'''''''''''''''''''''''''''''''''''
+
+.. autoclass:: cubicweb.web.formfields.RelationField()
+.. autofunction:: cubicweb.web.formfields.guess_field
+
 """
 __docformat__ = "restructuredtext en"
 
@@ -13,6 +52,7 @@
 from datetime import datetime
 
 from logilab.mtconverter import xml_escape
+from logilab.common import nullobject
 from logilab.common.date import ustrftime
 
 from yams.schema import KNOWN_METAATTRIBUTES, role_name
@@ -45,55 +85,80 @@
     result += sorted(partresult)
     return result
 
-_MARKER = object()
+_MARKER = nullobject()
 
 class Field(object):
     """This class is the abstract base class for all fields. It hold a bunch
     of attributes which may be used for fine control of the behaviour of a
     concret field.
 
+    **Attributes**
+
     All the attributes described below have sensible default value which may be
-    overriden by value given to field's constructor.
+    overriden by named arguments given to field's constructor.
 
-    :name:
-       name of the field (basestring), should be unique in a form.
-    :id:
-       dom identifier (default to the same value as `name`), should be unique in
+    :attr:`name`
+       base name of the field (basestring). The actual input name is returned by
+       the :meth:`input_name` method and may differ from that name (for instance
+       if `eidparam` is true).
+    :attr:`id`
+       DOM identifier (default to the same value as `name`), should be unique in
        a form.
-    :label:
+    :attr:`label`
        label of the field (default to the same value as `name`).
-    :help:
+    :attr:`help`
        help message about this field.
-    :widget:
+    :attr:`widget`
        widget associated to the field. Each field class has a default widget
        class which may be overriden per instance.
-    :required:
+    :attr:`value`
+       field value. May be an actual value or a callable which should take the
+       form as argument and return a value.
+    :attr:`choices`
+       static vocabulary for this field. May be a list of values, a list of
+       (label, value) tuples or a callable which should take the form and field
+       as arguments and return a list of values or a list of (label, value).
+    :attr:`required`
        bool flag telling if the field is required or not.
-    :value:
-       field value (may be an actual value, a default value or nothing)
-    :choices:
-       static vocabulary for this field. May be a list of values or a list of
-       (label, value) tuples if specified.
-    :sort:
+    :attr:`sort`
        bool flag telling if the vocabulary (either static vocabulary specified
        in `choices` or dynamic vocabulary fetched from the form) should be
        sorted on label.
-    :internationalizable:
+    :attr:`internationalizable`
        bool flag telling if the vocabulary labels should be translated using the
        current request language.
-    :eidparam:
+    :attr:`eidparam`
        bool flag telling if this field is linked to a specific entity
-    :role:
+    :attr:`role`
        when the field is linked to an entity attribute or relation, tells the
        role of the entity in the relation (eg 'subject' or 'object')
-    :fieldset:
+    :attr:`fieldset`
        optional fieldset to which this field belongs to
-    :order:
+    :attr:`order`
        key used by automatic forms to sort fields
-    :ignore_req_params:
+    :attr:`ignore_req_params`
        when true, this field won't consider value potentialy specified using
        request's form parameters (eg you won't be able to specify a value using for
        instance url like http://mywebsite.com/form?field=value)
+
+    .. currentmodule:: cubicweb.web.formfields
+
+    **Generic methods**
+
+    .. automethod:: Field.input_name
+    .. automethod:: Field.dom_id
+    .. automethod:: Field.actual_fields
+
+    **Form generation methods**
+
+    .. automethod:: form_init
+    .. automethod:: typed_value
+
+    **Post handling methods**
+
+    .. automethod:: process_posted
+    .. automethod:: process_form_value
+
     """
     # default widget associated to this class of fields. May be overriden per
     # instance
@@ -163,8 +228,11 @@
         return not isinstance(self.widget, fw.HiddenInput)
 
     def actual_fields(self, form):
-        """return actual fields composing this field in case of a compound
-        field, usually simply return self
+        """Fields may be composed of other fields. For instance the
+        :class:`~cubicweb.web.formfields.RichTextField` is containing a format
+        field to define the text format. This method returns actual fields that
+        should be considered for display / edition. It usually simply return
+        self.
         """
         yield self
 
@@ -189,7 +257,10 @@
         return self.widget
 
     def input_name(self, form, suffix=None):
-        """return 'qualified name' for this field"""
+        """Return the 'qualified name' for this field, e.g. something suitable
+        to use as HTML input name. You can specify a suffix that will be
+        included in the name when widget needs several inputs.
+        """
         # caching is necessary else we get some pb on entity creation :
         # entity.eid is modified from creation mark (eg 'X') to its actual eid
         # (eg 123), and then `field.input_name()` won't return the right key
@@ -218,7 +289,10 @@
         return self.name
 
     def dom_id(self, form, suffix=None):
-        """return an html dom identifier for this field"""
+        """Return the HTML DOM identifier for this field, e.g. something
+        suitable to use as HTML input id. You can specify a suffix that will be
+        included in the name when widget needs several inputs.
+        """
         id = self.id or self.role_name()
         if suffix is not None:
             id += suffix
@@ -227,6 +301,8 @@
         return id
 
     def typed_value(self, form, load_bytes=False):
+        """Return the correctly typed value for this field in the form context.
+        """
         if self.eidparam and self.role is not None:
             entity = form.edited_entity
             if form._cw.vreg.schema.rschema(self.name).final:
@@ -323,8 +399,8 @@
         return form._cw.encoding
 
     def form_init(self, form):
-        """method called before by build_context to trigger potential field
-        initialization requiring the form instance
+        """Method called at form initialization to trigger potential field
+        initialization requiring the form instance. Do nothing by default.
         """
         pass
 
@@ -358,7 +434,7 @@
         return True
 
     def process_form_value(self, form):
-        """process posted form and return correctly typed value"""
+        """Return the correctly typed value posted for this field."""
         try:
             return form.formvalues[(self, form)]
         except KeyError:
@@ -378,6 +454,9 @@
         return value or None
 
     def process_posted(self, form):
+        """Return an iterator on (field, value) that has been posted for
+        field returned by :meth:`~cubicweb.web.formfields.Field.actual_fields`.
+        """
         for field in self.actual_fields(form):
             if field is self:
                 try:
@@ -395,6 +474,20 @@
 
 
 class StringField(Field):
+    """Use this field to edit unicode string (`String` yams type). This field
+    additionaly support a `max_length` attribute that specify a maximum size for
+    the string (`None` meaning no limit).
+
+    Unless explicitly specified, the widget for this field will be:
+
+    * :class:`~cubicweb.web.formwidgets.Select` if some vocabulary is specified
+      using `choices` attribute
+
+    * :class:`~cubicweb.web.formwidgets.TextInput` if maximum size is specified
+      using `max_length` attribute and this length is inferior to 257.
+
+    * :class:`~cubicweb.web.formwidgets.TextArea` in all other cases
+    """
     widget = fw.TextArea
     size = 45
 
@@ -427,6 +520,12 @@
 
 
 class PasswordField(StringField):
+    """Use this field to edit password (`Password` yams type, encoded python
+    string).
+
+    Unless explicitly specified, the widget for this field will be
+    a :class:`~cubicweb.web.formwidgets.PasswordInput`.
+    """
     widget = fw.PasswordInput
     def form_init(self, form):
         if self.eidparam and form.edited_entity.has_eid():
@@ -444,6 +543,17 @@
 
 
 class RichTextField(StringField):
+    """This compound field allow edition of text (unicode string) in
+    a particular format. It has an inner field holding the text format,
+    that can be specified using `format_field` argument. If not specified
+    one will be automaticall generated.
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.FCKEditor` or a
+    :class:`~cubicweb.web.formwidgets.TextArea`. according to the field's
+    format and to user's preferences.
+    """
+
     widget = None
     def __init__(self, format_field=None, **kwargs):
         super(RichTextField, self).__init__(**kwargs)
@@ -515,6 +625,17 @@
 
 
 class FileField(StringField):
+    """This compound field allow edition of binary stream (`Bytes` yams
+    type). Three inner fields may be specified:
+
+    * `format_field`, holding the file's format.
+    * `encoding_field`, holding the file's content encoding.
+    * `name_field`, holding the file's name.
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.FileInput`. Inner fields, if any,
+    will be added to a drop down menu at the right of the file input.
+    """
     widget = fw.FileInput
     needs_multipart = True
 
@@ -601,7 +722,17 @@
         return value
 
 
+# XXX turn into a widget
 class EditableFileField(FileField):
+    """This compound field allow edition of binary stream as
+    :class:`~cubicweb.web.formfields.FileField` but expect that stream to
+    actually contains some text.
+
+    If the stream format is one of text/plain, text/html, text/rest,
+    then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionaly
+    displayed, allowing to directly the file's content when desired, instead
+    of choosing a file from user's file system.
+    """
     editable_formats = ('text/plain', 'text/html', 'text/rest')
 
     def render(self, form, renderer):
@@ -639,6 +770,13 @@
 
 
 class IntField(Field):
+    """Use this field to edit integers (`Int` yams type). This field additionaly
+    support `min` and `max` attributes that specify a minimum and/or maximum
+    value for the integer (`None` meaning no boundary).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.TextInput`.
+    """
     def __init__(self, min=None, max=None, **kwargs):
         super(IntField, self).__init__(**kwargs)
         self.min = min
@@ -660,6 +798,12 @@
 
 
 class BooleanField(Field):
+    """Use this field to edit booleans (`Boolean` yams type).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.Radio` with yes/no values. You
+    can change that values by specifing `choices`.
+    """
     widget = fw.Radio
 
     def vocabulary(self, form):
@@ -672,6 +816,13 @@
 
 
 class FloatField(IntField):
+    """Use this field to edit floats (`Float` yams type). This field additionaly
+    support `min` and `max` attributes as the
+    :class:`~cubicweb.web.formfields.IntField`.
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.TextInput`.
+    """
     def format_single_value(self, req, value):
         formatstr = req.property_value('ui.float-format')
         if value is None:
@@ -694,6 +845,11 @@
 
 
 class DateField(StringField):
+    """Use this field to edit date (`Date` yams type).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.JQueryDatePicker`.
+    """
     widget = fw.JQueryDatePicker
     format_prop = 'ui.date-format'
     etype = 'Date'
@@ -719,17 +875,47 @@
 
 
 class DateTimeField(DateField):
+    """Use this field to edit datetime (`Datetime` yams type).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.JQueryDateTimePicker`.
+    """
     widget = fw.JQueryDateTimePicker
     format_prop = 'ui.datetime-format'
     etype = 'Datetime'
 
 
 class TimeField(DateField):
+    """Use this field to edit time (`Time` yams type).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.JQueryTimePicker`.
+    """
     widget = fw.JQueryTimePicker
     format_prop = 'ui.time-format'
     etype = 'Time'
 
 
+# XXX use cases where we don't actually want a better widget?
+class CompoundField(Field):
+    """This field shouldn't be used directly, it's designed to hold inner
+    fields that should be conceptually groupped together.
+    """
+    def __init__(self, fields, *args, **kwargs):
+        super(CompoundField, self).__init__(*args, **kwargs)
+        self.fields = fields
+
+    def subfields(self, form):
+        return self.fields
+
+    def actual_fields(self, form):
+        # don't add [self] to actual fields, compound field is usually kinda
+        # virtual, all interesting values are in subfield. Skipping it may avoid
+        # error when processed by the editcontroller : it may be marked as required
+        # while it has no value, hence generating a false error.
+        return list(self.fields)
+
+
 # relation vocabulary helper functions #########################################
 
 def relvoc_linkedto(entity, rtype, role):
@@ -784,7 +970,11 @@
 
 
 class RelationField(Field):
-    """the relation field to edit non final relations of an entity"""
+    """Use this field to edit a relation of an entity.
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.Select`.
+    """
 
     @staticmethod
     def fromcardinality(card, **kwargs):
@@ -867,28 +1057,21 @@
             eids.add(typed_eid)
         return eids
 
-# XXX use cases where we don't actually want a better widget?
-class CompoundField(Field):
-    def __init__(self, fields, *args, **kwargs):
-        super(CompoundField, self).__init__(*args, **kwargs)
-        self.fields = fields
-
-    def subfields(self, form):
-        return self.fields
-
-    def actual_fields(self, form):
-        # don't add [self] to actual fields, compound field is usually kinda
-        # virtual, all interesting values are in subfield. Skipping it may avoid
-        # error when processed by the editcontroller : it may be marked as required
-        # while it has no value, hence generating a false error.
-        return list(self.fields)
-
 
 _AFF_KWARGS = uicfg.autoform_field_kwargs
 
 def guess_field(eschema, rschema, role='subject', skip_meta_attr=True, **kwargs):
-    """return the most adapated widget to edit the relation
-    'subjschema rschema objschema' according to information found in the schema
+    """This function return the most adapted field to edit the given relation
+    (`rschema`) where the given entity type (`eschema`) is the subject or object
+    (`role`).
+
+    The field is initialized according to information found in the schema,
+    though any value can be explicitly specified using `kwargs`.
+
+    The `skip_meta_attr` flag is used to specify wether this function should
+    return a field for attributes considered as a meta-attributes
+    (e.g. describing an other attribute, such as the format or file name of a
+    file (`Bytes`) attribute).
     """
     fieldclass = None
     rdef = eschema.rdef(rschema, role)