move fields and widgets to their own module tls-sprint
authorsylvain.thenault@logilab.fr
Wed, 11 Mar 2009 23:25:29 +0100
branchtls-sprint
changeset 1081 f2a85f52b9e5
parent 1074 c07f3accf04a
child 1082 07c21784787b
move fields and widgets to their own module
web/form.py
web/formfields.py
web/formwidgets.py
--- a/web/form.py	Wed Mar 11 19:54:30 2009 +0100
+++ b/web/form.py	Wed Mar 11 23:25:29 2009 +0100
@@ -251,535 +251,6 @@
 
 ###############################################################################
 
-from cubicweb.common import tags
-
-# widgets ############
-
-class FieldWidget(object):
-    needs_js = ()
-    needs_css = ()
-    setdomid = True
-    settabindex = True
-    
-    def __init__(self, attrs=None, setdomid=None, settabindex=None):
-        self.attrs = attrs or {}
-        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
-
-    def add_media(self, form):
-        """adds media (CSS & JS) required by this widget"""
-        req = form.req
-        if self.needs_js:
-            req.add_js(self.needs_js)
-        if self.needs_css:
-            req.add_css(self.needs_css)
-        
-    def render(self, form, field):
-        raise NotImplementedError
-
-    def _render_attrs(self, form, field):
-        name = form.context[field]['name']
-        values = form.context[field]['value']
-        if not isinstance(values, (tuple, list)):
-            values = (values,)
-        attrs = dict(self.attrs)
-        if self.setdomid:
-            attrs['id'] = form.context[field]['id']
-        if self.settabindex:
-            attrs['tabindex'] = form.req.next_tabindex()
-        return name, values, attrs
-
-class Input(FieldWidget):
-    type = None
-    
-    def render(self, form, field):
-        self.add_media(form)
-        name, values, attrs = self._render_attrs(form, field)
-        inputs = [tags.input(name=name, value=value, type=self.type, **attrs)
-                  for value in values]
-        return u'\n'.join(inputs)
-
-class TextInput(Input):
-    type = 'text'
-
-class PasswordInput(Input):
-    type = 'password'
-    
-    def render(self, form, field):
-        self.add_media(form)
-        name, values, attrs = self._render_attrs(form, field)
-        assert len(values) == 1
-        inputs = [tags.input(name=name, value=values[0], type=self.type, **attrs),
-                  '<br/>',
-                  tags.input(name=name+'-confirm', type=self.type, **attrs),
-                  '&nbsp;', tags.span(form.req._('confirm password'),
-                                      **{'class': 'emphasis'})]
-        return u'\n'.join(inputs)
-
-class FileInput(Input):
-    type = 'file'
-    
-    def _render_attrs(self, form, field):
-        # ignore value which makes no sense here (XXX even on form validation error?)
-        name, values, attrs = super(FileInput, self)._render_attrs(form, field)
-        return name, ('',), attrs
-        
-class HiddenInput(Input):
-    type = 'hidden'
-    setdomid = False # by default, don't set id attribute on hidden input
-    settabindex = False
-    
-class ButtonInput(Input):
-    type = 'button'
-
-class TextArea(FieldWidget):
-    def render(self, form, field):
-        name, values, attrs = self._render_attrs(form, field)
-        attrs.setdefault('onkeypress', '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')
-        return tags.textarea(value, name=name, **attrs)
-
-
-class FCKEditor(TextArea):
-    def __init__(self, *args, **kwargs):
-        super(FCKEditor, self).__init__(*args, **kwargs)
-        self.attrs['cubicweb:type'] = 'wysiwyg'
-    
-    def render(self, form, field):
-        form.req.fckeditor_config()
-        return super(FCKEditor, self).render(form, field)
-
-
-class Select(FieldWidget):
-    def __init__(self, attrs=None, multiple=False):
-        super(Select, self).__init__(attrs)
-        self.multiple = multiple
-        
-    def render(self, form, field):
-        name, curvalues, attrs = self._render_attrs(form, field)
-        options = []
-        for label, value in field.vocabulary(form):
-            if value in curvalues:
-                options.append(tags.option(label, value=value, selected='selected'))
-            else:
-                options.append(tags.option(label, value=value))
-        return tags.select(name=name, multiple=self.multiple,
-                           options=options, **attrs)
-
-
-class CheckBox(Input):
-    type = 'checkbox'
-    
-    def render(self, form, field):
-        name, curvalues, attrs = self._render_attrs(form, field)
-        options = []
-        for label, value in field.vocabulary(form):
-            if value in curvalues:
-                tag = tags.input(name=name, value=value, type=self.type,
-                                 checked='checked', **attrs)
-            else:
-                tag = tags.input(name=name, value=value, type=self.type,
-                                 **attrs)
-            options.append(tag + label)
-        return '<br/>\n'.join(options)
-
-        
-class Radio(Input):
-    type = 'radio'
-    setdomid = False
-    
-    def render(self, form, field):
-        name, curvalues, attrs = self._render_attrs(form, field)
-        options = []
-        for label, value in field.vocabulary(form):
-            if value in curvalues:
-                options.append(tags.input(name=name, type=self.type, value=value, checked='checked', **attrs))
-            else:
-                options.append(tags.option(name=name, type=self.type, value=value, **attrs))
-            options[-1] += label + '<br/>'
-        return '\n'.join(options)
-
-
-class DateTimePicker(TextInput):
-    monthnames = ('january', 'february', 'march', 'april',
-                  'may', 'june', 'july', 'august',
-                  'september', 'october', 'november', 'december')
-    daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
-                'friday', 'saturday', 'sunday')
-
-    needs_js = ('cubicweb.ajax.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"""
-        # import here to avoid dependancy from cubicweb-common to simplejson
-        _ = 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):
-        txtwidget = super(DateTimePicker, self).render(form, field)
-        self.add_localized_infos(form.req)
-        cal_button = self._render_calendar_popup(form, field)
-        return txtwidget + cal_button
-    
-    def _render_calendar_popup(self, form, field):
-        req = form.req
-        value = form.context[field]['rawvalue']
-        inputid = form.context[field]['id']
-        helperid = '%shelper' % inputid
-        if not value:
-            value = date.today()
-        year, month = value.year, value.month
-        onclick = "toggleCalendar('%s', '%s', %s, %s);" % (
-            helperid, inputid, year, 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,
-                   req.external_resource('CALENDAR_ICON'),
-                   req._('calendar'), helperid) )
-
-
-class AjaxWidget(FieldWidget):
-    def __init__(self, wdgtype, inputid=None, **kwargs):
-        super(AjaxWidget, self).__init__(**kwargs)
-        self.attrs.setdefault('class', 'widget')
-        self.attrs.setdefault('cubicweb:loadtype', 'auto')
-        self.attrs['cubicweb:wdgtype'] = wdgtype
-        if inputid is not None:
-            self.attrs['cubicweb:inputid'] = inputid
-            
-    def render(self, form, field):
-        self.add_media(form)
-        name, values, attrs = self._render_attrs(form, field)
-        return tags.div(**attrs)
-
-
-# fields ############
-
-class Field(object):
-    """field class is introduced to control what's displayed in edition form
-    """
-    widget = TextInput
-    needs_multipart = False
-    creation_rank = 0
-
-    def __init__(self, name=None, id=None, label=None,
-                 widget=None, required=False, initial=None,
-                 choices=None, help=None, eidparam=False):
-        self.required = required
-        if widget is not None:
-            self.widget = widget
-        if isinstance(self.widget, type):
-            self.widget = self.widget()
-        self.name = name
-        self.label = label or name
-        self.id = id or name
-        self.initial = initial
-        self.choices = choices
-        self.help = help
-        self.eidparam = eidparam
-        self.role = 'subject'
-        # global fields ordering in forms
-        self.creation_rank = Field.creation_rank
-        Field.creation_rank += 1
-
-    def set_name(self, name):
-        assert name
-        self.name = name
-        if not self.id:
-            self.id = name
-        if not self.label:
-            self.label = name
-            
-    def is_visible(self):
-        return not isinstance(self.widget, HiddenInput)
-    
-    def actual_fields(self, form):
-        yield self
-
-    def __unicode__(self):
-        return u'<%s name=%r label=%r id=%r initial=%r>' % (
-            self.__class__.__name__, self.name, self.label,
-            self.id, self.initial)
-
-    def __repr__(self):
-        return self.__unicode__().encode('utf-8')
-    
-    def format_value(self, req, value):
-        if isinstance(value, (list, tuple)):
-            return [self.format_single_value(req, val) for val in value]
-        return self.format_single_value(req, value)
-    
-    def format_single_value(self, req, value):
-        if value is None:
-            return u''
-        return unicode(value)
-
-    def get_widget(self, form):
-        return self.widget
-    
-    def example_format(self, req):
-        return u''
-
-    def render(self, form, renderer):
-        return self.get_widget(form).render(form, self)
-
-    def vocabulary(self, form):
-        if self.choices is not None:
-            return self.choices
-        return form.form_field_vocabulary(self)
-
-    
-class StringField(Field):
-    def __init__(self, max_length=None, **kwargs):
-        super(StringField, self).__init__(**kwargs)
-        self.max_length = max_length
-
-
-class TextField(Field):
-    widget = TextArea
-    def __init__(self, rows=10, cols=80, **kwargs):
-        super(TextField, self).__init__(**kwargs)
-        self.rows = rows
-        self.cols = cols
-
-
-class RichTextField(TextField):
-    widget = None
-    def __init__(self, format_field=None, **kwargs):
-        super(RichTextField, self).__init__(**kwargs)
-        self.format_field = format_field
-
-    def get_widget(self, form):
-        if self.widget is None:
-            if self.use_fckeditor(form):
-                return FCKEditor()
-            return TextArea()
-        return self.widget
-
-    def get_format_field(self, form):
-        if self.format_field:
-            return self.format_field
-        # we have to cache generated field since it's use as key in the
-        # context dictionnary
-        req = form.req
-        try:
-            return req.data[self]
-        except KeyError:
-            if self.use_fckeditor(form):
-                # if fckeditor is used and format field isn't explicitly
-                # deactivated, we want an hidden field for the format
-                widget = HiddenInput()
-                choices = None
-            else:
-                # else we want a format selector
-                # XXX compute vocabulary
-                widget = Select
-                choices = [(req._(format), format) for format in FormatConstraint().vocabulary(req=req)]
-            field = StringField(name=self.name + '_format', widget=widget,
-                                choices=choices)
-            req.data[self] = field
-            return field
-    
-    def actual_fields(self, form):
-        yield self
-        format_field = self.get_format_field(form)
-        if format_field:
-            yield format_field
-            
-    def use_fckeditor(self, form):
-        """return True if fckeditor should be used to edit entity's attribute named
-        `attr`, according to user preferences
-        """
-        if form.req.use_fckeditor():
-            return form.form_field_format(self) == 'text/html'
-        return False
-
-    def render(self, form, renderer):
-        format_field = self.get_format_field(form)
-        if format_field:
-            result = format_field.render(form, renderer)
-        else:
-            result = u''
-        return result + self.get_widget(form).render(form, self)
-
-    
-class FileField(StringField):
-    widget = FileInput
-    needs_multipart = True
-    
-    def __init__(self, format_field=None, encoding_field=None, **kwargs):
-        super(FileField, self).__init__(**kwargs)
-        self.format_field = format_field
-        self.encoding_field = encoding_field
-        
-    def actual_fields(self, form):
-        yield self
-        if self.format_field:
-            yield self.format_field
-        if self.encoding_field:
-            yield self.encoding_field
-
-    def render(self, form, renderer):
-        wdgs = [self.get_widget(form).render(form, self)]
-        if self.format_field or self.encoding_field:
-            divid = '%s-advanced' % form.context[self]['name']
-            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
-                        (html_escape(toggle_action(divid)),
-                         form.req._('show advanced fields'),
-                         html_escape(form.req.build_url('data/puce_down.png')),
-                         form.req._('show advanced fields')))
-            wdgs.append(u'<div id="%s" class="hidden">' % divid)
-            if self.format_field:
-                wdgs.append(self.render_subfield(form, self.format_field, renderer))
-            if self.encoding_field:
-                wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
-            wdgs.append(u'</div>')            
-        if not self.required and form.context[self]['value']:
-            # trick to be able to delete an uploaded file
-            wdgs.append(u'<br/>')
-            wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'],
-                                   type=u'checkbox'))
-            wdgs.append(form.req._('detach attached file'))
-        return u'\n'.join(wdgs)
-
-    def render_subfield(self, form, field, renderer):
-        return (renderer.render_label(form, field)
-                + field.render(form, renderer)
-                + renderer.render_help(form, field)
-                + u'<br/>')
-
-        
-class EditableFileField(FileField):
-    editable_formats = ('text/plain', 'text/html', 'text/rest')
-    
-    def render(self, form, renderer):
-        wdgs = [super(EditableFileField, self).render(form, renderer)]
-        if form.form_field_format(self) in self.editable_formats:
-            data = form.form_field_value(self, {}, load_bytes=True)
-            if data:
-                encoding = form.form_field_encoding(self)
-                try:
-                    form.context[self]['value'] = unicode(data.getvalue(), encoding)
-                except UnicodeError:
-                    pass
-                else:
-                    if not self.required:
-                        msg = form.req._(
-                            'You can either submit a new file using the browse button above'
-                            ', or choose to remove already uploaded file by checking the '
-                            '"detach attached file" check-box, or edit file content online '
-                            'with the widget below.')
-                    else:
-                        msg = form.req._(
-                            'You can either submit a new file using the browse button above'
-                            ', or edit file content online with the widget below.')
-                    wdgs.append(u'<p><b>%s</b></p>' % msg)
-                    wdgs.append(TextArea(setdomid=False).render(form, self))
-                    # XXX restore form context?
-        return '\n'.join(wdgs)
-
-        
-class IntField(Field):
-    def __init__(self, min=None, max=None, **kwargs):
-        super(IntField, self).__init__(**kwargs)
-        self.min = min
-        self.max = max
-
-
-class BooleanField(Field):
-    widget = Radio
-        
-    def vocabulary(self, form):
-        if self.choices:
-            return self.choices
-        return [(form.req._('yes'), '1'), (form.req._('no'), '')]
-
-
-class FloatField(IntField):    
-    def format_single_value(self, req, value):
-        formatstr = entity.req.property_value('ui.float-format')
-        if value is None:
-            return u''
-        return formatstr % float(value)
-
-    def render_example(self, req):
-        return self.format_value(req, 1.234)
-
-
-class DateField(StringField):
-    format_prop = 'ui.date-format'
-    widget = DateTimePicker
-    
-    def format_single_value(self, req, value):
-        return value and ustrftime(value, req.property_value(self.format_prop)) or u''
-
-    def render_example(self, req):
-        return self.format_value(req, datetime.now())
-
-
-class DateTimeField(DateField):
-    format_prop = 'ui.datetime-format'
-
-
-class TimeField(DateField):
-    format_prop = 'ui.datetime-format'
-
-
-class HiddenInitialValueField(Field):
-    def __init__(self, visible_field, name):
-        super(HiddenInitialValueField, self).__init__(name=name,
-                                                      widget=HiddenInput,
-                                                      eidparam=True)
-        self.visible_field = visible_field
-    
-                 
-class RelationField(Field):
-    def __init__(self, **kwargs):
-        super(RelationField, self).__init__(**kwargs)
-
-    @staticmethod
-    def fromcardinality(card, role, **kwargs):
-        return RelationField(widget=Select(multiple=card in '*+'),
-                             **kwargs)
-        
-    def vocabulary(self, form):
-        entity = form.entity
-        req = entity.req
-        # first see if its specified by __linkto form parameters
-        linkedto = entity.linked_to(self.name, self.role)
-        if linkedto:
-            entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto)
-            return [(entity.view('combobox'), entity.eid) for entity in entities]
-        # it isn't, check if the entity provides a method to get correct values
-        res = []
-        if not self.required:
-            res.append(('', INTERNAL_FIELD_VALUE))
-        # vocabulary doesn't include current values, add them
-        if entity.has_eid():
-            rset = entity.related(self.name, self.role)
-            relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
-        else:
-            relatedvocab = []
-        return res + form.form_field_vocabulary(self) + relatedvocab
-    
-    def format_single_value(self, req, value):
-        return value
-    
-# forms ############
 class metafieldsform(type):
     def __new__(mcs, name, bases, classdict):
         allfields = []
@@ -1214,67 +685,3 @@
         for button in form.form_buttons():
             w(u'<td>%s</td>\n' % button)
         w(u'</tr></table>')
-
-
-def stringfield_from_constraints(constraints, **kwargs):
-    field = None
-    for cstr in constraints:
-        if isinstance(cstr, StaticVocabularyConstraint):
-            return StringField(widget=Select(vocabulary=cstr.vocabulary),
-                               **kwargs)
-        if isinstance(cstr, SizeConstraint) and cstr.max is not None:
-            if cstr.max > 257:
-                field = textfield_from_constraint(cstr, **kwargs)
-            else:
-                field = StringField(max_length=cstr.max, **kwargs)
-    return field or TextField(**kwargs)
-        
-
-def textfield_from_constraint(constraint, **kwargs):
-    if 256 < constraint.max < 513:
-        rows, cols = 5, 60
-    else:
-        rows, cols = 10, 80
-    return TextField(rows, cols, **kwargs)
-
-
-def find_field(eclass, subjschema, rschema, role='subject'):
-    """return the most adapated widget to edit the relation
-    'subjschema rschema objschema' according to information found in the schema
-    """
-    fieldclass = None
-    if role == 'subject':
-        objschema = rschema.objects(subjschema)[0]
-        cardidx = 0
-    else:
-        objschema = rschema.subjects(subjschema)[0]
-        cardidx = 1
-    card = rschema.rproperty(subjschema, objschema, 'cardinality')[cardidx]
-    required = card in '1+'
-    if rschema in eclass.widgets:
-        fieldclass = eclass.widgets[rschema]
-        if isinstance(fieldclass, basestring):
-            return StringField(name=rschema.type)
-    elif not rschema.is_final():
-        return RelationField.fromcardinality(card, role,name=rschema.type,
-                                             required=required)
-    else:
-        fieldclass = FIELDS[objschema]
-    if fieldclass is StringField:
-        constraints = rschema.rproperty(subjschema, objschema, 'constraints')
-        return stringfield_from_constraints(constraints, name=rschema.type,
-                                            required=required)
-    return fieldclass(name=rschema.type, required=required)
-
-FIELDS = {
-    'Boolean':  BooleanField,
-    'Bytes':    FileField,
-    'Date':     DateField,
-    'Datetime': DateTimeField,
-    'Int':      IntField,
-    'Float':    FloatField,
-    'Decimal':  StringField,
-    'Password': StringField,
-    'String' :  StringField,
-    'Time':     TimeField,
-    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/formfields.py	Wed Mar 11 23:25:29 2009 +0100
@@ -0,0 +1,382 @@
+"""field classes for form construction
+
+:organization: Logilab
+:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+class Field(object):
+    """field class is introduced to control what's displayed in edition form
+    """
+    widget = TextInput
+    needs_multipart = False
+    creation_rank = 0
+
+    def __init__(self, name=None, id=None, label=None,
+                 widget=None, required=False, initial=None,
+                 choices=None, help=None, eidparam=False):
+        self.required = required
+        if widget is not None:
+            self.widget = widget
+        if isinstance(self.widget, type):
+            self.widget = self.widget()
+        self.name = name
+        self.label = label or name
+        self.id = id or name
+        self.initial = initial
+        self.choices = choices
+        self.help = help
+        self.eidparam = eidparam
+        self.role = 'subject'
+        # global fields ordering in forms
+        self.creation_rank = Field.creation_rank
+        Field.creation_rank += 1
+
+    def __unicode__(self):
+        return u'<%s name=%r label=%r id=%r initial=%r>' % (
+            self.__class__.__name__, self.name, self.label,
+            self.id, self.initial)
+
+    def __repr__(self):
+        return self.__unicode__().encode('utf-8')
+
+    def set_name(self, name):
+        assert name
+        self.name = name
+        if not self.id:
+            self.id = name
+        if not self.label:
+            self.label = name
+            
+    def is_visible(self):
+        return not isinstance(self.widget, HiddenInput)
+    
+    def actual_fields(self, form):
+        yield self
+    
+    def format_value(self, req, value):
+        if isinstance(value, (list, tuple)):
+            return [self.format_single_value(req, val) for val in value]
+        return self.format_single_value(req, value)
+    
+    def format_single_value(self, req, value):
+        if value is None:
+            return u''
+        return unicode(value)
+
+    def get_widget(self, form):
+        return self.widget
+    
+    def example_format(self, req):
+        return u''
+
+    def render(self, form, renderer):
+        return self.get_widget(form).render(form, self)
+
+    def vocabulary(self, form):
+        if self.choices is not None:
+            return self.choices
+        return form.form_field_vocabulary(self)
+
+    
+class StringField(Field):
+    def __init__(self, max_length=None, **kwargs):
+        super(StringField, self).__init__(**kwargs)
+        self.max_length = max_length
+
+
+class TextField(Field):
+    widget = TextArea
+    def __init__(self, rows=10, cols=80, **kwargs):
+        super(TextField, self).__init__(**kwargs)
+        self.rows = rows
+        self.cols = cols
+
+
+class RichTextField(TextField):
+    widget = None
+    def __init__(self, format_field=None, **kwargs):
+        super(RichTextField, self).__init__(**kwargs)
+        self.format_field = format_field
+
+    def get_widget(self, form):
+        if self.widget is None:
+            if self.use_fckeditor(form):
+                return FCKEditor()
+            return TextArea()
+        return self.widget
+
+    def get_format_field(self, form):
+        if self.format_field:
+            return self.format_field
+        # we have to cache generated field since it's use as key in the
+        # context dictionnary
+        req = form.req
+        try:
+            return req.data[self]
+        except KeyError:
+            if self.use_fckeditor(form):
+                # if fckeditor is used and format field isn't explicitly
+                # deactivated, we want an hidden field for the format
+                widget = HiddenInput()
+                choices = None
+            else:
+                # else we want a format selector
+                # XXX compute vocabulary
+                widget = Select
+                choices = [(req._(format), format) for format in FormatConstraint().vocabulary(req=req)]
+            field = StringField(name=self.name + '_format', widget=widget,
+                                choices=choices)
+            req.data[self] = field
+            return field
+    
+    def actual_fields(self, form):
+        yield self
+        format_field = self.get_format_field(form)
+        if format_field:
+            yield format_field
+            
+    def use_fckeditor(self, form):
+        """return True if fckeditor should be used to edit entity's attribute named
+        `attr`, according to user preferences
+        """
+        if form.req.use_fckeditor():
+            return form.form_field_format(self) == 'text/html'
+        return False
+
+    def render(self, form, renderer):
+        format_field = self.get_format_field(form)
+        if format_field:
+            result = format_field.render(form, renderer)
+        else:
+            result = u''
+        return result + self.get_widget(form).render(form, self)
+
+    
+class FileField(StringField):
+    widget = FileInput
+    needs_multipart = True
+    
+    def __init__(self, format_field=None, encoding_field=None, **kwargs):
+        super(FileField, self).__init__(**kwargs)
+        self.format_field = format_field
+        self.encoding_field = encoding_field
+        
+    def actual_fields(self, form):
+        yield self
+        if self.format_field:
+            yield self.format_field
+        if self.encoding_field:
+            yield self.encoding_field
+
+    def render(self, form, renderer):
+        wdgs = [self.get_widget(form).render(form, self)]
+        if self.format_field or self.encoding_field:
+            divid = '%s-advanced' % form.context[self]['name']
+            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
+                        (html_escape(toggle_action(divid)),
+                         form.req._('show advanced fields'),
+                         html_escape(form.req.build_url('data/puce_down.png')),
+                         form.req._('show advanced fields')))
+            wdgs.append(u'<div id="%s" class="hidden">' % divid)
+            if self.format_field:
+                wdgs.append(self.render_subfield(form, self.format_field, renderer))
+            if self.encoding_field:
+                wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
+            wdgs.append(u'</div>')            
+        if not self.required and form.context[self]['value']:
+            # trick to be able to delete an uploaded file
+            wdgs.append(u'<br/>')
+            wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'],
+                                   type=u'checkbox'))
+            wdgs.append(form.req._('detach attached file'))
+        return u'\n'.join(wdgs)
+
+    def render_subfield(self, form, field, renderer):
+        return (renderer.render_label(form, field)
+                + field.render(form, renderer)
+                + renderer.render_help(form, field)
+                + u'<br/>')
+
+        
+class EditableFileField(FileField):
+    editable_formats = ('text/plain', 'text/html', 'text/rest')
+    
+    def render(self, form, renderer):
+        wdgs = [super(EditableFileField, self).render(form, renderer)]
+        if form.form_field_format(self) in self.editable_formats:
+            data = form.form_field_value(self, {}, load_bytes=True)
+            if data:
+                encoding = form.form_field_encoding(self)
+                try:
+                    form.context[self]['value'] = unicode(data.getvalue(), encoding)
+                except UnicodeError:
+                    pass
+                else:
+                    if not self.required:
+                        msg = form.req._(
+                            'You can either submit a new file using the browse button above'
+                            ', or choose to remove already uploaded file by checking the '
+                            '"detach attached file" check-box, or edit file content online '
+                            'with the widget below.')
+                    else:
+                        msg = form.req._(
+                            'You can either submit a new file using the browse button above'
+                            ', or edit file content online with the widget below.')
+                    wdgs.append(u'<p><b>%s</b></p>' % msg)
+                    wdgs.append(TextArea(setdomid=False).render(form, self))
+                    # XXX restore form context?
+        return '\n'.join(wdgs)
+
+        
+class IntField(Field):
+    def __init__(self, min=None, max=None, **kwargs):
+        super(IntField, self).__init__(**kwargs)
+        self.min = min
+        self.max = max
+
+
+class BooleanField(Field):
+    widget = Radio
+        
+    def vocabulary(self, form):
+        if self.choices:
+            return self.choices
+        return [(form.req._('yes'), '1'), (form.req._('no'), '')]
+
+
+class FloatField(IntField):    
+    def format_single_value(self, req, value):
+        formatstr = entity.req.property_value('ui.float-format')
+        if value is None:
+            return u''
+        return formatstr % float(value)
+
+    def render_example(self, req):
+        return self.format_value(req, 1.234)
+
+
+class DateField(StringField):
+    format_prop = 'ui.date-format'
+    widget = DateTimePicker
+    
+    def format_single_value(self, req, value):
+        return value and ustrftime(value, req.property_value(self.format_prop)) or u''
+
+    def render_example(self, req):
+        return self.format_value(req, datetime.now())
+
+
+class DateTimeField(DateField):
+    format_prop = 'ui.datetime-format'
+
+
+class TimeField(DateField):
+    format_prop = 'ui.datetime-format'
+
+
+class HiddenInitialValueField(Field):
+    def __init__(self, visible_field, name):
+        super(HiddenInitialValueField, self).__init__(name=name,
+                                                      widget=HiddenInput,
+                                                      eidparam=True)
+        self.visible_field = visible_field
+    
+                 
+class RelationField(Field):
+    def __init__(self, **kwargs):
+        super(RelationField, self).__init__(**kwargs)
+
+    @staticmethod
+    def fromcardinality(card, role, **kwargs):
+        return RelationField(widget=Select(multiple=card in '*+'),
+                             **kwargs)
+        
+    def vocabulary(self, form):
+        entity = form.entity
+        req = entity.req
+        # first see if its specified by __linkto form parameters
+        linkedto = entity.linked_to(self.name, self.role)
+        if linkedto:
+            entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto)
+            return [(entity.view('combobox'), entity.eid) for entity in entities]
+        # it isn't, check if the entity provides a method to get correct values
+        res = []
+        if not self.required:
+            res.append(('', INTERNAL_FIELD_VALUE))
+        # vocabulary doesn't include current values, add them
+        if entity.has_eid():
+            rset = entity.related(self.name, self.role)
+            relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
+        else:
+            relatedvocab = []
+        return res + form.form_field_vocabulary(self) + relatedvocab
+    
+    def format_single_value(self, req, value):
+        return value
+
+
+def stringfield_from_constraints(constraints, **kwargs):
+    field = None
+    for cstr in constraints:
+        if isinstance(cstr, StaticVocabularyConstraint):
+            return StringField(widget=Select(vocabulary=cstr.vocabulary),
+                               **kwargs)
+        if isinstance(cstr, SizeConstraint) and cstr.max is not None:
+            if cstr.max > 257:
+                field = textfield_from_constraint(cstr, **kwargs)
+            else:
+                field = StringField(max_length=cstr.max, **kwargs)
+    return field or TextField(**kwargs)
+        
+
+def textfield_from_constraint(constraint, **kwargs):
+    if 256 < constraint.max < 513:
+        rows, cols = 5, 60
+    else:
+        rows, cols = 10, 80
+    return TextField(rows, cols, **kwargs)
+
+
+def find_field(eclass, subjschema, rschema, role='subject'):
+    """return the most adapated widget to edit the relation
+    'subjschema rschema objschema' according to information found in the schema
+    """
+    fieldclass = None
+    if role == 'subject':
+        objschema = rschema.objects(subjschema)[0]
+        cardidx = 0
+    else:
+        objschema = rschema.subjects(subjschema)[0]
+        cardidx = 1
+    card = rschema.rproperty(subjschema, objschema, 'cardinality')[cardidx]
+    required = card in '1+'
+    if rschema in eclass.widgets:
+        fieldclass = eclass.widgets[rschema]
+        if isinstance(fieldclass, basestring):
+            return StringField(name=rschema.type)
+    elif not rschema.is_final():
+        return RelationField.fromcardinality(card, role,name=rschema.type,
+                                             required=required)
+    else:
+        fieldclass = FIELDS[objschema]
+    if fieldclass is StringField:
+        constraints = rschema.rproperty(subjschema, objschema, 'constraints')
+        return stringfield_from_constraints(constraints, name=rschema.type,
+                                            required=required)
+    return fieldclass(name=rschema.type, required=required)
+
+FIELDS = {
+    'Boolean':  BooleanField,
+    'Bytes':    FileField,
+    'Date':     DateField,
+    'Datetime': DateTimeField,
+    'Int':      IntField,
+    'Float':    FloatField,
+    'Decimal':  StringField,
+    'Password': StringField,
+    'String' :  StringField,
+    'Time':     TimeField,
+    }
+views/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/formwidgets.py	Wed Mar 11 23:25:29 2009 +0100
@@ -0,0 +1,220 @@
+"""widget classes for form construction
+
+:organization: Logilab
+:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.common import tags
+
+class FieldWidget(object):
+    needs_js = ()
+    needs_css = ()
+    setdomid = True
+    settabindex = True
+    
+    def __init__(self, attrs=None, setdomid=None, settabindex=None):
+        self.attrs = attrs or {}
+        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
+
+    def add_media(self, form):
+        """adds media (CSS & JS) required by this widget"""
+        req = form.req
+        if self.needs_js:
+            req.add_js(self.needs_js)
+        if self.needs_css:
+            req.add_css(self.needs_css)
+        
+    def render(self, form, field):
+        raise NotImplementedError
+
+    def _render_attrs(self, form, field):
+        name = form.context[field]['name']
+        values = form.context[field]['value']
+        if not isinstance(values, (tuple, list)):
+            values = (values,)
+        attrs = dict(self.attrs)
+        if self.setdomid:
+            attrs['id'] = form.context[field]['id']
+        if self.settabindex:
+            attrs['tabindex'] = form.req.next_tabindex()
+        return name, values, attrs
+
+class Input(FieldWidget):
+    type = None
+    
+    def render(self, form, field):
+        self.add_media(form)
+        name, values, attrs = self._render_attrs(form, field)
+        inputs = [tags.input(name=name, value=value, type=self.type, **attrs)
+                  for value in values]
+        return u'\n'.join(inputs)
+
+class TextInput(Input):
+    type = 'text'
+
+class PasswordInput(Input):
+    type = 'password'
+    
+    def render(self, form, field):
+        self.add_media(form)
+        name, values, attrs = self._render_attrs(form, field)
+        assert len(values) == 1
+        inputs = [tags.input(name=name, value=values[0], type=self.type, **attrs),
+                  '<br/>',
+                  tags.input(name=name+'-confirm', type=self.type, **attrs),
+                  '&nbsp;', tags.span(form.req._('confirm password'),
+                                      **{'class': 'emphasis'})]
+        return u'\n'.join(inputs)
+
+class FileInput(Input):
+    type = 'file'
+    
+    def _render_attrs(self, form, field):
+        # ignore value which makes no sense here (XXX even on form validation error?)
+        name, values, attrs = super(FileInput, self)._render_attrs(form, field)
+        return name, ('',), attrs
+        
+class HiddenInput(Input):
+    type = 'hidden'
+    setdomid = False # by default, don't set id attribute on hidden input
+    settabindex = False
+    
+class ButtonInput(Input):
+    type = 'button'
+
+class TextArea(FieldWidget):
+    def render(self, form, field):
+        name, values, attrs = self._render_attrs(form, field)
+        attrs.setdefault('onkeypress', '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')
+        return tags.textarea(value, name=name, **attrs)
+
+
+class FCKEditor(TextArea):
+    def __init__(self, *args, **kwargs):
+        super(FCKEditor, self).__init__(*args, **kwargs)
+        self.attrs['cubicweb:type'] = 'wysiwyg'
+    
+    def render(self, form, field):
+        form.req.fckeditor_config()
+        return super(FCKEditor, self).render(form, field)
+
+
+class Select(FieldWidget):
+    def __init__(self, attrs=None, multiple=False):
+        super(Select, self).__init__(attrs)
+        self.multiple = multiple
+        
+    def render(self, form, field):
+        name, curvalues, attrs = self._render_attrs(form, field)
+        options = []
+        for label, value in field.vocabulary(form):
+            if value in curvalues:
+                options.append(tags.option(label, value=value, selected='selected'))
+            else:
+                options.append(tags.option(label, value=value))
+        return tags.select(name=name, multiple=self.multiple,
+                           options=options, **attrs)
+
+
+class CheckBox(Input):
+    type = 'checkbox'
+    
+    def render(self, form, field):
+        name, curvalues, attrs = self._render_attrs(form, field)
+        options = []
+        for label, value in field.vocabulary(form):
+            if value in curvalues:
+                tag = tags.input(name=name, value=value, type=self.type,
+                                 checked='checked', **attrs)
+            else:
+                tag = tags.input(name=name, value=value, type=self.type,
+                                 **attrs)
+            options.append(tag + label)
+        return '<br/>\n'.join(options)
+
+        
+class Radio(Input):
+    type = 'radio'
+    setdomid = False
+    
+    def render(self, form, field):
+        name, curvalues, attrs = self._render_attrs(form, field)
+        options = []
+        for label, value in field.vocabulary(form):
+            if value in curvalues:
+                options.append(tags.input(name=name, type=self.type, value=value, checked='checked', **attrs))
+            else:
+                options.append(tags.option(name=name, type=self.type, value=value, **attrs))
+            options[-1] += label + '<br/>'
+        return '\n'.join(options)
+
+
+class DateTimePicker(TextInput):
+    monthnames = ('january', 'february', 'march', 'april',
+                  'may', 'june', 'july', 'august',
+                  'september', 'october', 'november', 'december')
+    daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
+                'friday', 'saturday', 'sunday')
+
+    needs_js = ('cubicweb.ajax.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"""
+        # import here to avoid dependancy from cubicweb-common to simplejson
+        _ = 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):
+        txtwidget = super(DateTimePicker, self).render(form, field)
+        self.add_localized_infos(form.req)
+        cal_button = self._render_calendar_popup(form, field)
+        return txtwidget + cal_button
+    
+    def _render_calendar_popup(self, form, field):
+        req = form.req
+        value = form.context[field]['rawvalue']
+        inputid = form.context[field]['id']
+        helperid = '%shelper' % inputid
+        if not value:
+            value = date.today()
+        year, month = value.year, value.month
+        onclick = "toggleCalendar('%s', '%s', %s, %s);" % (
+            helperid, inputid, year, 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,
+                   req.external_resource('CALENDAR_ICON'),
+                   req._('calendar'), helperid) )
+
+
+class AjaxWidget(FieldWidget):
+    def __init__(self, wdgtype, inputid=None, **kwargs):
+        super(AjaxWidget, self).__init__(**kwargs)
+        self.attrs.setdefault('class', 'widget')
+        self.attrs.setdefault('cubicweb:loadtype', 'auto')
+        self.attrs['cubicweb:wdgtype'] = wdgtype
+        if inputid is not None:
+            self.attrs['cubicweb:inputid'] = inputid
+            
+    def render(self, form, field):
+        self.add_media(form)
+        name, values, attrs = self._render_attrs(form, field)
+        return tags.div(**attrs)