diff -r 000000000000 -r b97547f5f1fa web/widgets.py
--- /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'' % (
+ 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'' % (forattr, label)
+ else:
+ label = u'' % (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'%s' % errex.errors[self.name]
+ return u''
+
+ def render_help(self, entity):
+ """render a help message about the (edited) field"""
+ req = entity.req
+ help = [u'
']
+ descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description')
+ if descr:
+ help.append(u'%s' % req._(descr))
+ example = self.render_example(req)
+ if example:
+ help.append(u'(%s: %s)'
+ % (req._('sample format'), example))
+ return u' '.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'\n' % (
+ editmark, self.rname, qvalue)
+
+class InputWidget(Widget):
+ """abstract class for input generating a 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' % (
+ 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'' % 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' % {
+ '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
\n (%s)' % (
+ 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'\n'\
+ '\n' % (
+ frname, format, frname)
+ return u'%s' % (
+ 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
' % (
+ 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'%s
' % (self.rname, attrs1, entity.req._('yes')),
+ u'%s
' % (self.rname, attrs2, entity.req._('no'))]
+ return '\n'.join(wdgs)
+
+
+class FileWidget(Widget):
+ need_multipart = True
+ def _file_wdg(self, entity):
+ wdgs = [u'' % (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'' %
+ (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'
%s
' % 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'