web/widgets.py
author Pierre-Yves David <pierre-yves.david@logilab.fr>
Thu, 03 Dec 2009 09:50:51 +0100
branchstable
changeset 3991 a0aa9789c6bc
parent 3689 deb13e88e037
child 4212 ab6573088b4a
permissions -rw-r--r--
Pasting blog about facet in the facet section of the doc book.

"""widgets for entity edition

those are in cubicweb.common since we need to know available widgets at schema
serialization time

:organization: Logilab
:copyright: 2001-2009 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
"""
__docformat__ = "restructuredtext en"

from datetime import datetime

from logilab.mtconverter import xml_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'<div class="helper">']
        descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description')
        if descr:
            help.append(u'<span>%s</span>' % req._(descr))
        example = self.render_example(req)
        if example:
            help.append(u'<span>(%s: %s)</span>'
                        % (req._('sample format'), example))
        help.append(u'</div>')
        return u'&#160;'.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.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 = xml_escape(value)
        if isinstance(dvalue, basestring):
            dvalue = xml_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 = xml_escape(value)
        if isinstance(dvalue, basestring):
            dvalue = xml_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': xml_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"/>&#160;<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 _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 = xml_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 = xml_escape(dvalue)
        if entity.use_fckeditor(self.name):
            entity.req.fckeditor_config()
            if with_format:
                if entity.has_eid():
                    format = entity.attr_metadata(self.name, 'format')
                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" onkeyup="autogrow(this)" name="%s" %s>%s</textarea>' % (
                hidden, self.rname, self.format_attrs(), dvalue)
        if with_format and entity.e_schema.has_metadata(self.name, 'format'):
            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 onkeyup="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):
    html_attributes = Widget.html_attributes | set(('disabled',))
    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.e_schema.has_metadata(self.name, 'format')
            or entity.e_schema.has_metadata(self.name, 'encoding')):
            divid = '%s-%s-advanced' % (self.name, entity.eid)
            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
                        (xml_escape(toggle_action(divid)),
                         req._('show advanced fields'),
                         xml_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():
            if 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 %s' % entity.dc_title()))
            else:
                wdgs.append(u'<br/>')
                wdgs.append(req._('currently attached file: %s' % entity.dc_title()))
        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.attr_metadata(self.name, 'format') 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.attr_metadata(self.name, 'encoding')
                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, xml_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=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.entity_from_eid(eid) 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, xml_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">&#160;</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')

    @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 __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(str(formatstr))]
        return values

    def render_example(self, req):
        formatstr = req.property_value(self.format_key)
        return datetime.now().strftime(str(formatstr))


    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'<div class="helper">' ]
        descr = self.rschema.rproperty(self.subjtype, self.objtype, 'description')
        if descr:
            help.append('<span>%s</span>' % req._(descr))
        example = self.render_example(req)
        if example:
            help.append('<span>(%s: %s)</span>'
                        % (req._('sample format'), example))
        help.append(u'</div>')
        return u'&#160;'.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 = datetime.now()
        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 __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
        kwattrs['size'] = 16
        kwattrs['maxlength'] = 16
        DateWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)

    def render_example(self, req):
        formatstr1 = req.property_value('ui.datetime-format')
        formatstr2 = req.property_value('ui.date-format')
        return req._('%(fmt1)s, or without time: %(fmt2)s') % {
            'fmt1': datetime.now().strftime(str(formatstr1)),
            'fmt2': datetime.now().strftime(str(formatstr2)),
            }


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 = xml_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 = xml_escape(entity.build_url('embed', url=url))
        return u'<a href="%s">%s</a>' % (aurl, url)



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 getattr(eclass, 'widgets', ()):
        wcls = WIDGETS[eclass.widgets[rschema]]
    elif not rschema.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())