web/widgets.py
changeset 0 b97547f5f1fa
child 62 ef06f71533d9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/widgets.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,981 @@
+"""widgets for entity edition
+
+those are in cubicweb.common since we need to know available widgets at schema
+serialization time
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from simplejson import dumps
+from mx.DateTime import now, today
+
+from logilab.mtconverter import html_escape
+
+from yams.constraints import SizeConstraint, StaticVocabularyConstraint
+
+from cubicweb.common.uilib import toggle_action
+from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
+
+def _format_attrs(kwattrs):
+    """kwattrs is the dictionary of the html attributes available for
+    the edited element
+    """
+    # sort for predictability (required for tests)
+    return u' '.join(sorted(u'%s="%s"' % item for item in kwattrs.iteritems()))
+
+def _value_from_values(values):
+    # take care, value may be 0, 0.0...
+    if values:
+        value = values[0]
+        if value is None:
+            value = u''
+    else:
+        value = u''
+    return value
+
+def _eclass_eschema(eschema_or_eclass):
+    try:
+        return eschema_or_eclass, eschema_or_eclass.e_schema
+    except AttributeError:
+        return None, eschema_or_eclass
+
+def checkbox(name, value, attrs='', checked=None):
+    if checked is None:
+        checked = value
+    checked = checked and 'checked="checked"' or ''
+    return u'<input type="checkbox" name="%s" value="%s" %s %s />' % (
+        name, value, checked, attrs)
+
+def widget(vreg, subjschema, rschema, objschema, role='object'):
+    """get a widget to edit the given relation"""
+    if rschema == 'eid':
+        # return HiddenWidget(vreg, subjschema, rschema, objschema)
+        return EidWidget(vreg, _eclass_eschema(subjschema)[1], rschema, objschema)
+    return widget_factory(vreg, subjschema, rschema, objschema, role=role)
+
+
+class Widget(object):
+    """abstract widget class"""
+    need_multipart = False
+    # generate the "id" attribute with the same value as the "name" (html) attribute
+    autoid = True
+    html_attributes = set(('id', 'class', 'tabindex', 'accesskey', 'onchange', 'onkeypress'))
+    cubicwebns_attributes = set()
+    
+    def __init__(self, vreg, subjschema, rschema, objschema,
+                 role='subject', description=None,
+                 **kwattrs):
+        self.vreg = vreg
+        self.rschema = rschema
+        self.subjtype = subjschema
+        self.objtype = objschema
+        self.role = role
+        self.name = rschema.type
+        self.description = description
+        self.attrs = kwattrs
+        # XXX accesskey may not be unique
+        kwattrs['accesskey'] = self.name[0]
+
+    def copy(self):
+        """shallow copy (useful when you need to modify self.attrs
+        because widget instances are cached)
+        """
+        # brute force copy (subclasses don't have the
+        # same __init__ prototype) 
+        widget = self.__new__(self.__class__)
+        widget.__dict__ = dict(self.__dict__)
+        widget.attrs = dict(widget.attrs)
+        return widget
+    
+    @staticmethod
+    def size_constraint_attrs(attrs, maxsize):
+        """set html attributes in the attrs dict to consider maxsize"""
+        pass
+
+    def format_attrs(self):
+        """return a string with html attributes available for the edit input"""
+        # sort for predictability (required for tests)
+        attrs = []
+        for name, value in self.attrs.iteritems():
+            # namespace attributes have priority over standard xhtml ones
+            if name in self.cubicwebns_attributes:
+                attrs.append(u'cubicweb:%s="%s"' % (name, value))
+            elif name in self.html_attributes:
+                attrs.append(u'%s="%s"' % (name, value))
+        return u' '.join(sorted(attrs))
+        
+    def required(self, entity):
+        """indicates if the widget needs a value to be filled in"""
+        card = self.rschema.cardinality(self.subjtype, self.objtype, self.role)
+        return card in '1+'
+
+    def input_id(self, entity):
+        try:
+            return self.rname
+        except AttributeError:
+            return eid_param(self.name, entity.eid)
+    
+    def render_label(self, entity, label=None):
+        """render widget's label"""
+        label = label or self.rschema.display_name(entity.req, self.role)
+        forid = self.input_id(entity)
+        if forid:
+            forattr =  ' for="%s"' % forid
+        else:
+            forattr = ''
+        if self.required(entity):
+            label = u'<label class="required"%s>%s</label>' % (forattr, label)
+        else:
+            label = u'<label%s>%s</label>' % (forattr, label)
+        return label
+    
+    def render_error(self, entity):
+        """return validation error for widget's field of the given entity, if
+        any
+        """
+        errex = entity.req.data.get('formerrors')
+        if errex and errex.eid == entity.eid and self.name in errex.errors:
+            entity.req.data['displayederrors'].add(self.name)
+            return u'<span class="error">%s</span>' % errex.errors[self.name]
+        return u''
+
+    def render_help(self, entity):
+        """render a help message about the (edited) field"""
+        req = entity.req
+        help = [u'<br/>']
+        descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description')
+        if descr:
+            help.append(u'<span class="helper">%s</span>' % req._(descr))
+        example = self.render_example(req)
+        if example:
+            help.append(u'<span class="helper">(%s: %s)</span>'
+                        % (req._('sample format'), example))
+        return u'&nbsp;'.join(help)
+    
+    def render_example(self, req):
+        return u''
+        
+    def render(self, entity):
+        """render the widget for a simple view"""
+        if not entity.has_eid():
+            return u''
+        return entity.printable_value(self.name)
+    
+    def edit_render(self, entity, tabindex=None,
+                    includehelp=False, useid=None, **kwargs):
+        """render the widget for edition"""
+        # this is necessary to handle multiple edition
+        self.rname = eid_param(self.name, entity.eid)
+        if useid:
+            self.attrs['id'] = useid
+        elif self.autoid:
+            self.attrs['id'] = self.rname
+        if tabindex is not None:
+            self.attrs['tabindex'] = tabindex
+        else:
+            self.attrs['tabindex'] = entity.req.next_tabindex()
+        output = self._edit_render(entity, **kwargs)
+        if includehelp:
+            output += self.render_help(entity)
+        return output
+    
+    def _edit_render(self, entity):
+        """do the actual job to render the widget for edition"""
+        raise NotImplementedError
+
+    def current_values(self, entity):
+        """return the value of the field associated to this widget on the given
+        entity. always return a list of values, which'll have size equal to 1
+        if the field is monovalued (like all attribute fields, but not all non
+        final relation fields
+        """
+        if self.rschema.is_final():
+            return entity.attribute_values(self.name)
+        elif entity.has_eid():
+            return [row[0] for row in entity.related(self.name, self.role)]
+        return ()
+            
+    def current_value(self, entity):
+        return _value_from_values(self.current_values(entity))
+
+    def current_display_values(self, entity):
+        """same as .current_values but consider values stored in session in case
+        of validation error
+        """
+        values = entity.req.data.get('formvalues')
+        if values is None:
+            return self.current_values(entity)
+        cdvalues = values.get(self.rname)
+        if cdvalues is None:
+            return self.current_values(entity)
+        if not isinstance(cdvalues, (list, tuple)):
+            cdvalues = (cdvalues,)
+        return cdvalues
+    
+    def current_display_value(self, entity):
+        """same as .current_value but consider values stored in session in case
+        of validation error
+        """
+        return _value_from_values(self.current_display_values(entity))
+    
+    def hidden_input(self, entity, qvalue):
+        """return an hidden field which
+        1. indicates that a field is edited
+        2. hold the old value to easily detect if the field has been modified
+
+        `qvalue` is the html quoted old value
+        """
+        if self.role == 'subject':
+            editmark = 'edits'
+        else:
+            editmark = 'edito'
+        if qvalue is None or not entity.has_eid():
+            qvalue = INTERNAL_FIELD_VALUE
+        return u'<input type="hidden" name="%s-%s" value="%s"/>\n' % (
+            editmark, self.rname, qvalue)
+
+class InputWidget(Widget):
+    """abstract class for input generating a <input> tag"""
+    input_type = None
+    html_attributes = Widget.html_attributes | set(('type', 'name', 'value'))
+
+    def _edit_render(self, entity):
+        value = self.current_value(entity)
+        dvalue = self.current_display_value(entity)
+        if isinstance(value, basestring):
+            value = html_escape(value)
+        if isinstance(dvalue, basestring):
+            dvalue = html_escape(dvalue)
+        return u'%s<input type="%s" name="%s" value="%s" %s/>' % (
+            self.hidden_input(entity, value), self.input_type,
+            self.rname, dvalue, self.format_attrs())
+
+class HiddenWidget(InputWidget):
+    input_type = 'hidden'
+    autoid = False
+    def __init__(self, vreg, subjschema, rschema, objschema,
+                 role='subject', **kwattrs):
+        InputWidget.__init__(self, vreg, subjschema, rschema, objschema,
+                             role='subject', 
+                             **kwattrs)
+        # disable access key
+        del self.attrs['accesskey']
+
+    def current_value(self, entity):
+        value = InputWidget.current_value(self, entity)
+        return value or INTERNAL_FIELD_VALUE
+
+    def current_display_value(self, entity):
+        value = InputWidget.current_display_value(self, entity)
+        return value or INTERNAL_FIELD_VALUE
+    
+    def render_label(self, entity, label=None):
+        """render widget's label"""
+        return u''
+    
+    def render_help(self, entity):
+        return u''
+    
+    def hidden_input(self, entity, value):
+        """no hidden input for hidden input"""
+        return ''
+    
+
+class EidWidget(HiddenWidget):
+
+    def _edit_render(self, entity):
+        return u'<input type="hidden" name="eid" value="%s" />' % entity.eid
+
+
+class StringWidget(InputWidget):
+    input_type = 'text'
+    html_attributes = InputWidget.html_attributes | set(('size', 'maxlength'))
+    @staticmethod
+    def size_constraint_attrs(attrs, maxsize):
+        """set html attributes in the attrs dict to consider maxsize"""
+        attrs['size'] = min(maxsize, 40)
+        attrs['maxlength'] = maxsize
+        
+        
+class AutoCompletionWidget(StringWidget):
+    cubicwebns_attributes = (StringWidget.cubicwebns_attributes |
+                          set(('accesskey', 'size', 'maxlength')))
+    attrs = ()
+    
+    wdgtype = 'SuggestField'
+    
+    def current_value(self, entity):
+        value = StringWidget.current_value(self, entity)
+        return value or INTERNAL_FIELD_VALUE
+
+    def _get_url(self, entity):
+        return entity.req.build_url('json', fname=entity.autocomplete_initfuncs[self.rschema],
+                                pageid=entity.req.pageid, mode='remote')
+
+    def _edit_render(self, entity):
+        req = entity.req
+        req.add_js( ('cubicweb.widgets.js', 'jquery.autocomplete.js') )
+        req.add_css('jquery.autocomplete.css')
+        value = self.current_value(entity)
+        dvalue = self.current_display_value(entity)
+        if isinstance(value, basestring):
+            value = html_escape(value)
+        if isinstance(dvalue, basestring):
+            dvalue = html_escape(dvalue)
+        iid = self.attrs.pop('id')
+        if self.required(entity):
+            cssclass = u' required'
+        else:
+            cssclass = u''
+        dataurl = self._get_url(entity)
+        return (u'%(hidden)s<input type="text" name="%(iid)s" value="%(value)s" cubicweb:dataurl="%(url)s" class="widget%(required)s" id="%(iid)s" '
+                u'tabindex="%(tabindex)s" cubicweb:loadtype="auto" cubicweb:wdgtype="%(wdgtype)s"  %(attrs)s />' % {
+                    'iid': iid,
+                    'hidden': self.hidden_input(entity, value),
+                    'wdgtype': self.wdgtype,
+                    'url': html_escape(dataurl),
+                    'tabindex': self.attrs.pop('tabindex'),
+                    'value': dvalue,
+                    'attrs': self.format_attrs(),
+                    'required' : cssclass,
+                    })
+
+class StaticFileAutoCompletionWidget(AutoCompletionWidget):
+    wdgtype = 'StaticFileSuggestField'
+    
+    def _get_url(self, entity):
+        return entity.req.datadir_url + entity.autocomplete_initfuncs[self.rschema]
+
+class RestrictedAutoCompletionWidget(AutoCompletionWidget):
+    wdgtype = 'RestrictedSuggestField'    
+
+    
+class PasswordWidget(InputWidget):
+    input_type = 'password'
+    
+    def required(self, entity):
+        if InputWidget.required(self, entity) and not entity.has_eid():
+            return True
+        return False
+    
+    def current_values(self, entity):
+        # on existant entity, show password field has non empty (we don't have
+        # the actual value
+        if entity.has_eid():
+            return (INTERNAL_FIELD_VALUE,)
+        return super(PasswordWidget, self).current_values(entity)
+
+    def _edit_render(self, entity):
+        html = super(PasswordWidget, self)._edit_render(entity)
+        name = eid_param(self.name + '-confirm', entity.eid)
+        return u'%s<br/>\n<input type="%s" name="%s" id="%s" tabindex="%s"/>&nbsp;<span class="emphasis">(%s)</span>' % (
+            html, self.input_type, name, name, entity.req.next_tabindex(),
+            entity.req._('confirm password'))
+
+    
+class TextWidget(Widget):
+    html_attributes = Widget.html_attributes | set(('rows', 'cols'))
+    
+    @staticmethod
+    def size_constraint_attrs(attrs, maxsize):
+        """set html attributes in the attrs dict to consider maxsize"""
+        if 256 < maxsize < 513:
+            attrs['cols'], attrs['rows'] = 60, 5
+        else:
+            attrs['cols'], attrs['rows'] = 80, 10
+    
+    def render(self, entity):
+        if not entity.has_eid():
+            return u''
+        return entity.printable_value(self.name)
+    
+    def add_fckeditor_info(self, req):
+        req.add_js('fckeditor.js')
+        req.fckeditor_config()
+    
+    def _edit_render(self, entity, with_format=True):
+        req = entity.req
+        editor = self._edit_render_textarea(entity, with_format)
+        value = self.current_value(entity)
+        if isinstance(value, basestring):
+            value = html_escape(value)
+        return u'%s%s' % (self.hidden_input(entity, value), editor)
+    
+    def _edit_render_textarea(self, entity, with_format):
+        self.attrs.setdefault('cols', 80)
+        self.attrs.setdefault('rows', 20)
+        dvalue = self.current_display_value(entity)
+        if isinstance(dvalue, basestring):
+            dvalue = html_escape(dvalue)
+        if entity.use_fckeditor(self.name):
+            self.add_fckeditor_info(entity.req)
+            if with_format:
+                if entity.has_eid():
+                    format = entity.format(self.name)
+                else:
+                    format = ''
+                frname = eid_param(self.name + '_format', entity.eid)
+                hidden = u'<input type="hidden" name="edits-%s" value="%s"/>\n'\
+                         '<input type="hidden" name="%s" value="text/html"/>\n' % (
+                    frname, format, frname)
+            return u'%s<textarea cubicweb:type="wysiwyg" onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % (
+                hidden, self.rname, self.format_attrs(), dvalue)
+        if with_format and entity.has_format(self.name):
+            fmtwdg = entity.get_widget(self.name + '_format')
+            fmtwdgstr = fmtwdg.edit_render(entity, tabindex=self.attrs['tabindex'])
+            self.attrs['tabindex'] = entity.req.next_tabindex()
+        else:
+            fmtwdgstr = ''
+        return u'%s<br/><textarea onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % (
+            fmtwdgstr, self.rname, self.format_attrs(), dvalue)
+            
+    
+class CheckBoxWidget(Widget):
+    html_attributes = Widget.html_attributes | set(('checked', ))
+    def _edit_render(self, entity):
+        value = self.current_value(entity)
+        dvalue = self.current_display_value(entity)
+        return self.hidden_input(entity, value) + checkbox(self.rname, 'checked', self.format_attrs(), dvalue)
+
+    def render(self, entity):
+        if not entity.has_eid():
+            return u''
+        if getattr(entity, self.name):
+            return entity.req._('yes')
+        return entity.req._('no')
+
+
+class YesNoRadioWidget(CheckBoxWidget):
+    
+    def _edit_render(self, entity):
+        value = self.current_value(entity)
+        dvalue = self.current_display_value(entity)
+        attrs1 = self.format_attrs()
+        del self.attrs['id'] # avoid duplicate id for xhtml compliance
+        attrs2 = self.format_attrs()
+        if dvalue:
+            attrs1 += ' checked="checked"'
+        else:
+            attrs2 += ' checked="checked"'
+        wdgs = [self.hidden_input(entity, value),
+                u'<input type="radio" name="%s" value="1" %s/>%s<br/>' % (self.rname, attrs1, entity.req._('yes')),
+                u'<input type="radio" name="%s" value="" %s/>%s<br/>' % (self.rname, attrs2, entity.req._('no'))]
+        return '\n'.join(wdgs)
+
+    
+class FileWidget(Widget):
+    need_multipart = True
+    def _file_wdg(self, entity):
+        wdgs = [u'<input type="file" name="%s" %s/>' % (self.rname, self.format_attrs())]
+        req = entity.req
+        if entity.has_format(self.name) or entity.has_text_encoding(self.name):
+            divid = '%s-%s-advanced' % (self.name, entity.eid)
+            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
+                        (html_escape(toggle_action(divid)),
+                         req._('show advanced fields'),
+                         html_escape(req.build_url('data/puce_down.png')),
+                         req._('show advanced fields')))
+            wdgs.append(u'<div id="%s" class="hidden">' % divid)
+            for extraattr in ('_format', '_encoding'):
+                if entity.e_schema.has_subject_relation('%s%s' % (self.name, extraattr)):
+                    ewdg = entity.get_widget(self.name + extraattr)
+                    wdgs.append(ewdg.render_label(entity))
+                    wdgs.append(ewdg.edit_render(entity, includehelp=True))
+                    wdgs.append(u'<br/>')
+            wdgs.append(u'</div>')
+        if entity.has_eid() and not self.required(entity):
+            # trick to be able to delete an uploaded file
+            wdgs.append(u'<br/>')
+            wdgs.append(checkbox(eid_param('__%s_detach' % self.rname, entity.eid), False))
+            wdgs.append(req._('detach attached file'))
+        return '\n'.join(wdgs)
+    
+    def _edit_render(self, entity):
+        return self.hidden_input(entity, None) + self._file_wdg(entity)
+
+
+class TextFileWidget(FileWidget):
+    def _edit_msg(self, entity):
+        if entity.has_eid() and not self.required(entity):
+            msg = entity.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 = entity.req._(
+                'You can either submit a new file using the browse button above'
+                ', or edit file content online with the widget below.')
+        return msg
+    
+    def _edit_render(self, entity):
+        wdgs = [self._file_wdg(entity)]
+        if entity.format(self.name) in ('text/plain', 'text/html', 'text/rest'):
+            msg = self._edit_msg(entity)
+            wdgs.append(u'<p><b>%s</b></p>' % msg)
+            twdg = TextWidget(self.vreg, self.subjtype, self.rschema, self.objtype)
+            twdg.rname = self.rname
+            data = getattr(entity, self.name)
+            if data:
+                encoding = entity.text_encoding(self.name)
+                try:
+                    entity[self.name] = unicode(data.getvalue(), encoding)
+                except UnicodeError:
+                    pass
+                else:
+                    wdgs.append(twdg.edit_render(entity, with_format=False))
+                    entity[self.name] = data # restore Binary value
+            wdgs.append(u'<br/>')
+        return '\n'.join(wdgs)
+
+
+class ComboBoxWidget(Widget):
+    html_attributes = Widget.html_attributes | set(('multiple', 'size'))
+    
+    def __init__(self, vreg, subjschema, rschema, objschema,
+                 multiple=False, **kwattrs):
+        super(ComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
+                                             **kwattrs)
+        if multiple:
+            self.attrs['multiple'] = 'multiple'
+            if not 'size' in self.attrs:
+                self.attrs['size'] = '5'
+        # disable access key (dunno why but this is not allowed by xhtml 1.0)
+        del self.attrs['accesskey']
+        
+    def vocabulary(self, entity):
+        raise NotImplementedError()
+    
+    def form_value(self, entity, value, values):
+        if value in values:
+            flag = 'selected="selected"'
+        else:
+            flag = ''
+        return value, flag
+
+    def _edit_render(self, entity):
+        values = self.current_values(entity)
+        if values:
+            res = [self.hidden_input(entity, v) for v in values]
+        else:
+            res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
+        dvalues = self.current_display_values(entity)
+        res.append(u'<select name="%s" %s>' % (self.rname, self.format_attrs()))
+        for label, value in self.vocabulary(entity):
+            if value is None:
+                # handle separator
+                res.append(u'<optgroup label="%s"/>' % (label or ''))
+            else:
+                value, flag = self.form_value(entity, value, dvalues)
+                res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label)))
+        res.append(u'</select>')
+        return '\n'.join(res)
+
+ 
+class StaticComboBoxWidget(ComboBoxWidget):
+    
+    def __init__(self, vreg, subjschema, rschema, objschema,
+                 vocabfunc, multiple=False, sort=False, **kwattrs):
+        super(StaticComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
+                                                   multiple, **kwattrs)
+        self.sort = sort
+        self.vocabfunc = vocabfunc
+
+    def vocabulary(self, entity):
+        choices = self.vocabfunc(entity)
+        if self.sort:
+            choices = sorted(choices)
+        if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'):
+            return zip((entity.req._(v) for v in choices), choices)
+        return zip(choices, choices)
+    
+
+class EntityLinkComboBoxWidget(ComboBoxWidget):
+    """to be used be specific forms"""
+    
+    def current_values(self, entity):
+        if entity.has_eid():
+            return [r[0] for r in entity.related(self.name, self.role)]
+        defaultmeth = 'default_%s_%s' % (self.role, self.name)
+        if hasattr(entity, defaultmeth):
+            return getattr(entity, defaultmeth)()
+        return ()
+    
+    def vocabulary(self, entity):
+        return [('', INTERNAL_FIELD_VALUE)] + entity.vocabulary(self.rschema, self.role)
+
+
+class RawDynamicComboBoxWidget(EntityLinkComboBoxWidget):
+    
+    def vocabulary(self, entity, limit=None):
+        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
+        if not self.required(entity):
+            res = [('', INTERNAL_FIELD_VALUE)]
+        else:
+            res = []
+        # 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 + entity.vocabulary(self.rschema, self.role) + relatedvocab
+
+
+class DynamicComboBoxWidget(RawDynamicComboBoxWidget):
+    
+    def vocabulary(self, entity, limit=None):
+        return sorted(super(DynamicComboBoxWidget, self).vocabulary(entity, limit))
+
+
+class AddComboBoxWidget(DynamicComboBoxWidget):
+    def _edit_render(self, entity):
+        req = entity.req
+        req.add_js( ('cubicweb.ajax.js', 'jquery.js', 'cubicweb.widgets.js') )
+        values = self.current_values(entity)
+        if values:
+            res = [self.hidden_input(entity, v) for v in values]
+        else:
+            res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
+        dvalues = self.current_display_values(entity)
+        etype_from = entity.e_schema.subject_relation(self.name).objects(entity.e_schema)[0]
+        res.append(u'<select class="widget" cubicweb:etype_to="%s" cubicweb:etype_from="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="AddComboBox" name="%s" %s>'
+                   % (entity.e_schema, etype_from, self.rname, self.format_attrs()))
+        for label, value in self.vocabulary(entity):
+            if value is None:
+                # handle separator
+                res.append(u'<optgroup label="%s"/>' % (label or ''))
+            else:
+                value, flag = self.form_value(entity, value, dvalues)
+                res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label)))
+        res.append(u'</select>')
+        res.append(u'<div id="newvalue">')
+        res.append(u'<input type="text" id="newopt" />')
+        res.append(u'<a href="javascript:noop()" id="add_newopt">&nbsp;</a></div>')
+        return '\n'.join(res)
+
+class IntegerWidget(StringWidget):
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs['size'] = 5
+        kwattrs['maxlength'] = 15
+        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+        
+    def render_example(self, req):
+        return '23'
+    
+
+        
+class FloatWidget(StringWidget):
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs['size'] = 5
+        kwattrs['maxlength'] = 15
+        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+
+    def render_example(self, req):
+        formatstr = req.property_value('ui.float-format')
+        return formatstr % 1.23
+    
+    def current_values(self, entity):
+        values = entity.attribute_values(self.name)
+        if values:
+            formatstr = entity.req.property_value('ui.float-format')
+            value = values[0]
+            if value is not None:
+                value = float(value)
+            else:
+                return ()
+            return [formatstr % value]
+        return ()
+
+class DecimalWidget(StringWidget):
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs['size'] = 5
+        kwattrs['maxlength'] = 15
+        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+        
+    def render_example(self, req):
+        return '345.0300'
+    
+
+
+class DateWidget(StringWidget):
+    format_key = 'ui.date-format'
+    monthnames = ("january", "february", "march", "april",
+                  "may", "june", "july", "august",
+                  "september", "october", "november", "december")
+    
+    daynames = ("monday", "tuesday", "wednesday", "thursday",
+                "friday", "saturday", "sunday")
+    
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs.setdefault('size', 10)
+        kwattrs.setdefault('maxlength', 10)
+        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+
+    def current_values(self, entity):
+        values = entity.attribute_values(self.name)
+        if values and hasattr(values[0], 'strftime'):
+            formatstr = entity.req.property_value(self.format_key)
+            return [values[0].strftime(formatstr)]
+        return values
+
+    def render_example(self, req):
+        formatstr = req.property_value(self.format_key)
+        return now().strftime(formatstr)
+
+    def add_localized_infos(self, 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 self.monthnames]
+        daynames = [_(dname) for dname in self.daynames]
+        req.html_headers.define_var('MONTHNAMES', monthnames)
+        req.html_headers.define_var('DAYNAMES', daynames)
+
+
+    def _edit_render(self, entity):
+        wdg = super(DateWidget, self)._edit_render(entity)
+        cal_button = self.render_calendar_popup(entity)
+        return wdg+cal_button
+
+    def render_help(self, entity):
+        """calendar popup widget"""
+        req = entity.req
+        help = [ u'<br/>' ]
+        descr = self.rschema.rproperty(self.subjtype, self.objtype, 'description')
+        if descr:
+            help.append('<span class="helper">%s</span>' % req._(descr))
+        example = self.render_example(req)
+        if example:
+            help.append('<span class="helper">(%s: %s)</span>'
+                        % (req._('sample format'), example))
+        return u'&nbsp;'.join(help)
+
+    def render_calendar_popup(self, entity):
+        """calendar popup widget"""
+        req = entity.req
+        self.add_localized_infos(req)
+        req.add_js(('cubicweb.ajax.js', 'cubicweb.calendar.js',))
+        req.add_css(('cubicweb.calendar_popup.css',))
+        inputid = self.attrs.get('id', self.rname)
+        helperid = "%shelper" % inputid
+        _today = today()
+        year = int(req.form.get('year', _today.year))
+        month = int(req.form.get('month', _today.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 DateTimeWidget(DateWidget):
+    format_key = 'ui.datetime-format'
+    
+    def render_example(self, req):
+        formatstr1 = req.property_value('ui.datetime-format')
+        formatstr2 = req.property_value('ui.date-format')
+        return req._('%s, or without time: %s') % (now().strftime(formatstr1),
+                                                   now().strftime(formatstr2))
+
+
+
+
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs['size'] = 16
+        kwattrs['maxlength'] = 16
+        DateWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+
+
+class TimeWidget(StringWidget):
+    format_key = 'ui.time-format'
+    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
+        kwattrs['size'] = 5
+        kwattrs['maxlength'] = 5
+        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
+
+        
+class EmailWidget(StringWidget):
+    
+    def render(self, entity):
+        email = getattr(entity, self.name)
+        if not email:
+            return u''
+        return u'<a href="mailto:%s">%s</a>' % (email, email)
+        
+class URLWidget(StringWidget):
+    
+    def render(self, entity):
+        url = getattr(entity, self.name)
+        if not url:
+            return u''
+        url = html_escape(url)
+        return u'<a href="%s">%s</a>' % (url, url)
+    
+class EmbededURLWidget(StringWidget):
+    
+    def render(self, entity):
+        url = getattr(entity, self.name)
+        if not url:
+            return u''
+        aurl = html_escape(entity.build_url('embed', url=url))
+        return u'<a href="%s">%s</a>' % (aurl, url)
+
+
+
+class PropertyKeyWidget(ComboBoxWidget):
+    """specific widget for EProperty.pkey field to set the value widget according to
+    the selected key
+    """
+    
+    def _edit_render(self, entity):
+        entity.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
+        vtabindex = self.attrs.get('tabindex', 0) + 1
+        self.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % (
+            entity.eid, vtabindex)
+        # limit size
+        if not entity.has_eid():
+            self.attrs['size'] = 10
+        else:
+            self.attrs['size'] = 1
+        return super(PropertyKeyWidget, self)._edit_render(entity)
+    
+    def vocabulary(self, entity):
+        _ = entity.req._
+        if entity.has_eid():
+            return [(_(entity.pkey), entity.pkey)]
+        # key beginning with 'system.' should usually not be edited by hand
+        choices = entity.vreg.user_property_keys()
+        return sorted(zip((_(v) for v in choices), choices))
+
+
+class PropertyValueWidget(Widget):
+    """specific widget for EProperty.value field which will be different according to
+    the selected key type and vocabulary information
+    """
+    
+    def render_help(self, entity):
+        return u''
+        
+    def render(self, entity):
+        assert entity.has_eid()
+        w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs)
+        return w.render(entity)
+        
+    def _edit_render(self, entity):
+        if not entity.has_eid():
+            # no key set yet, just include an empty div which will be filled
+            # on key selection
+            # empty span as well else html validation fail (label is refering to this id)
+            return u'<div id="div:%s"><span id="%s"/></div>' % (self.rname, self.attrs.get('id'))
+        w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs)
+        if entity.pkey.startswith('system.'):
+            value = '<span class="value" id="%s">%s</span>' % (self.attrs.get('id'), w.render(entity))
+            msg = entity.req._('value associated to this key is not editable manually')
+            return value + '<div>%s</div>' % msg
+        return w.edit_render(entity, self.attrs.get('tabindex'), includehelp=True)
+    
+
+def widget_factory(vreg, subjschema, rschema, objschema, role='subject',
+                   **kwargs):
+    """return the most adapated widget to edit the relation
+    'subjschema rschema objschema' according to information found in the schema
+    """
+    if role == 'subject':
+        eclass, subjschema = _eclass_eschema(subjschema)
+    else:
+        eclass, objschema = _eclass_eschema(objschema)
+    if eclass is not None and rschema in eclass.widgets:
+        wcls = WIDGETS[eclass.widgets[rschema]]
+    elif not rschema.is_final():
+        card = rschema.rproperty(subjschema, objschema, 'cardinality')
+        if role == 'object':
+            multiple = card[1] in '+*'
+        else: #if role == 'subject':
+            multiple = card[0] in '+*'
+        return DynamicComboBoxWidget(vreg, subjschema, rschema, objschema,
+                                     role=role, multiple=multiple)
+    else:
+        wcls = None
+    factory = FACTORIES.get(objschema, _default_widget_factory)
+    return factory(vreg, subjschema, rschema, objschema, wcls=wcls,
+                   role=role, **kwargs)
+
+
+# factories to find the most adapated widget according to a type and other constraints
+                
+def _string_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
+    w = None
+    for c in rschema.rproperty(subjschema, objschema, 'constraints'):
+        if isinstance(c, StaticVocabularyConstraint):
+            # may have been set by a previous SizeConstraint but doesn't make sense
+            # here (even doesn't have the same meaning on a combobox actually)
+            kwargs.pop('size', None) 
+            return (wcls or StaticComboBoxWidget)(vreg, subjschema, rschema, objschema,
+                                                  vocabfunc=c.vocabulary, **kwargs)
+        if isinstance(c, SizeConstraint) and c.max is not None:
+            # don't return here since a StaticVocabularyConstraint may
+            # follow
+            if wcls is None:
+                if c.max < 257:
+                    _wcls = StringWidget
+                else:
+                    _wcls = TextWidget
+            else:
+                _wcls = wcls
+            _wcls.size_constraint_attrs(kwargs, c.max)
+            w = _wcls(vreg, subjschema, rschema, objschema, **kwargs)
+    if w is None:
+        w = (wcls or TextWidget)(vreg, subjschema, rschema, objschema, **kwargs)
+    return w
+
+def _default_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
+    if wcls is None:
+        wcls = _WFACTORIES[objschema]
+    return wcls(vreg, subjschema, rschema, objschema, **kwargs)
+
+FACTORIES = {
+    'String' :  _string_widget_factory,
+    'Boolean':  _default_widget_factory,
+    'Bytes':    _default_widget_factory,
+    'Date':     _default_widget_factory,
+    'Datetime': _default_widget_factory,
+    'Float':    _default_widget_factory,
+    'Decimal':    _default_widget_factory,
+    'Int':      _default_widget_factory,
+    'Password': _default_widget_factory,
+    'Time':     _default_widget_factory,
+    }
+
+# default widget by entity's type
+_WFACTORIES = {
+    'Boolean':  YesNoRadioWidget,
+    'Bytes':    FileWidget,
+    'Date':     DateWidget,
+    'Datetime': DateTimeWidget,
+    'Int':      IntegerWidget,
+    'Float':    FloatWidget,
+    'Decimal':  DecimalWidget,
+    'Password': PasswordWidget,
+    'String' :  StringWidget,
+    'Time':     TimeWidget,
+    }
+    
+# widgets registry
+WIDGETS = {}
+def register(widget_list):
+    for obj in widget_list:
+        if isinstance(obj, type) and issubclass(obj, Widget):
+            if obj is Widget or obj is ComboBoxWidget:
+                continue
+            WIDGETS[obj.__name__] = obj
+
+register(globals().values())