web/widgets.py
changeset 0 b97547f5f1fa
child 62 ef06f71533d9
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """widgets for entity edition
       
     2 
       
     3 those are in cubicweb.common since we need to know available widgets at schema
       
     4 serialization time
       
     5 
       
     6 :organization: Logilab
       
     7 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     8 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     9 """
       
    10 __docformat__ = "restructuredtext en"
       
    11 
       
    12 from simplejson import dumps
       
    13 from mx.DateTime import now, today
       
    14 
       
    15 from logilab.mtconverter import html_escape
       
    16 
       
    17 from yams.constraints import SizeConstraint, StaticVocabularyConstraint
       
    18 
       
    19 from cubicweb.common.uilib import toggle_action
       
    20 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
       
    21 
       
    22 def _format_attrs(kwattrs):
       
    23     """kwattrs is the dictionary of the html attributes available for
       
    24     the edited element
       
    25     """
       
    26     # sort for predictability (required for tests)
       
    27     return u' '.join(sorted(u'%s="%s"' % item for item in kwattrs.iteritems()))
       
    28 
       
    29 def _value_from_values(values):
       
    30     # take care, value may be 0, 0.0...
       
    31     if values:
       
    32         value = values[0]
       
    33         if value is None:
       
    34             value = u''
       
    35     else:
       
    36         value = u''
       
    37     return value
       
    38 
       
    39 def _eclass_eschema(eschema_or_eclass):
       
    40     try:
       
    41         return eschema_or_eclass, eschema_or_eclass.e_schema
       
    42     except AttributeError:
       
    43         return None, eschema_or_eclass
       
    44 
       
    45 def checkbox(name, value, attrs='', checked=None):
       
    46     if checked is None:
       
    47         checked = value
       
    48     checked = checked and 'checked="checked"' or ''
       
    49     return u'<input type="checkbox" name="%s" value="%s" %s %s />' % (
       
    50         name, value, checked, attrs)
       
    51 
       
    52 def widget(vreg, subjschema, rschema, objschema, role='object'):
       
    53     """get a widget to edit the given relation"""
       
    54     if rschema == 'eid':
       
    55         # return HiddenWidget(vreg, subjschema, rschema, objschema)
       
    56         return EidWidget(vreg, _eclass_eschema(subjschema)[1], rschema, objschema)
       
    57     return widget_factory(vreg, subjschema, rschema, objschema, role=role)
       
    58 
       
    59 
       
    60 class Widget(object):
       
    61     """abstract widget class"""
       
    62     need_multipart = False
       
    63     # generate the "id" attribute with the same value as the "name" (html) attribute
       
    64     autoid = True
       
    65     html_attributes = set(('id', 'class', 'tabindex', 'accesskey', 'onchange', 'onkeypress'))
       
    66     cubicwebns_attributes = set()
       
    67     
       
    68     def __init__(self, vreg, subjschema, rschema, objschema,
       
    69                  role='subject', description=None,
       
    70                  **kwattrs):
       
    71         self.vreg = vreg
       
    72         self.rschema = rschema
       
    73         self.subjtype = subjschema
       
    74         self.objtype = objschema
       
    75         self.role = role
       
    76         self.name = rschema.type
       
    77         self.description = description
       
    78         self.attrs = kwattrs
       
    79         # XXX accesskey may not be unique
       
    80         kwattrs['accesskey'] = self.name[0]
       
    81 
       
    82     def copy(self):
       
    83         """shallow copy (useful when you need to modify self.attrs
       
    84         because widget instances are cached)
       
    85         """
       
    86         # brute force copy (subclasses don't have the
       
    87         # same __init__ prototype) 
       
    88         widget = self.__new__(self.__class__)
       
    89         widget.__dict__ = dict(self.__dict__)
       
    90         widget.attrs = dict(widget.attrs)
       
    91         return widget
       
    92     
       
    93     @staticmethod
       
    94     def size_constraint_attrs(attrs, maxsize):
       
    95         """set html attributes in the attrs dict to consider maxsize"""
       
    96         pass
       
    97 
       
    98     def format_attrs(self):
       
    99         """return a string with html attributes available for the edit input"""
       
   100         # sort for predictability (required for tests)
       
   101         attrs = []
       
   102         for name, value in self.attrs.iteritems():
       
   103             # namespace attributes have priority over standard xhtml ones
       
   104             if name in self.cubicwebns_attributes:
       
   105                 attrs.append(u'cubicweb:%s="%s"' % (name, value))
       
   106             elif name in self.html_attributes:
       
   107                 attrs.append(u'%s="%s"' % (name, value))
       
   108         return u' '.join(sorted(attrs))
       
   109         
       
   110     def required(self, entity):
       
   111         """indicates if the widget needs a value to be filled in"""
       
   112         card = self.rschema.cardinality(self.subjtype, self.objtype, self.role)
       
   113         return card in '1+'
       
   114 
       
   115     def input_id(self, entity):
       
   116         try:
       
   117             return self.rname
       
   118         except AttributeError:
       
   119             return eid_param(self.name, entity.eid)
       
   120     
       
   121     def render_label(self, entity, label=None):
       
   122         """render widget's label"""
       
   123         label = label or self.rschema.display_name(entity.req, self.role)
       
   124         forid = self.input_id(entity)
       
   125         if forid:
       
   126             forattr =  ' for="%s"' % forid
       
   127         else:
       
   128             forattr = ''
       
   129         if self.required(entity):
       
   130             label = u'<label class="required"%s>%s</label>' % (forattr, label)
       
   131         else:
       
   132             label = u'<label%s>%s</label>' % (forattr, label)
       
   133         return label
       
   134     
       
   135     def render_error(self, entity):
       
   136         """return validation error for widget's field of the given entity, if
       
   137         any
       
   138         """
       
   139         errex = entity.req.data.get('formerrors')
       
   140         if errex and errex.eid == entity.eid and self.name in errex.errors:
       
   141             entity.req.data['displayederrors'].add(self.name)
       
   142             return u'<span class="error">%s</span>' % errex.errors[self.name]
       
   143         return u''
       
   144 
       
   145     def render_help(self, entity):
       
   146         """render a help message about the (edited) field"""
       
   147         req = entity.req
       
   148         help = [u'<br/>']
       
   149         descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description')
       
   150         if descr:
       
   151             help.append(u'<span class="helper">%s</span>' % req._(descr))
       
   152         example = self.render_example(req)
       
   153         if example:
       
   154             help.append(u'<span class="helper">(%s: %s)</span>'
       
   155                         % (req._('sample format'), example))
       
   156         return u'&nbsp;'.join(help)
       
   157     
       
   158     def render_example(self, req):
       
   159         return u''
       
   160         
       
   161     def render(self, entity):
       
   162         """render the widget for a simple view"""
       
   163         if not entity.has_eid():
       
   164             return u''
       
   165         return entity.printable_value(self.name)
       
   166     
       
   167     def edit_render(self, entity, tabindex=None,
       
   168                     includehelp=False, useid=None, **kwargs):
       
   169         """render the widget for edition"""
       
   170         # this is necessary to handle multiple edition
       
   171         self.rname = eid_param(self.name, entity.eid)
       
   172         if useid:
       
   173             self.attrs['id'] = useid
       
   174         elif self.autoid:
       
   175             self.attrs['id'] = self.rname
       
   176         if tabindex is not None:
       
   177             self.attrs['tabindex'] = tabindex
       
   178         else:
       
   179             self.attrs['tabindex'] = entity.req.next_tabindex()
       
   180         output = self._edit_render(entity, **kwargs)
       
   181         if includehelp:
       
   182             output += self.render_help(entity)
       
   183         return output
       
   184     
       
   185     def _edit_render(self, entity):
       
   186         """do the actual job to render the widget for edition"""
       
   187         raise NotImplementedError
       
   188 
       
   189     def current_values(self, entity):
       
   190         """return the value of the field associated to this widget on the given
       
   191         entity. always return a list of values, which'll have size equal to 1
       
   192         if the field is monovalued (like all attribute fields, but not all non
       
   193         final relation fields
       
   194         """
       
   195         if self.rschema.is_final():
       
   196             return entity.attribute_values(self.name)
       
   197         elif entity.has_eid():
       
   198             return [row[0] for row in entity.related(self.name, self.role)]
       
   199         return ()
       
   200             
       
   201     def current_value(self, entity):
       
   202         return _value_from_values(self.current_values(entity))
       
   203 
       
   204     def current_display_values(self, entity):
       
   205         """same as .current_values but consider values stored in session in case
       
   206         of validation error
       
   207         """
       
   208         values = entity.req.data.get('formvalues')
       
   209         if values is None:
       
   210             return self.current_values(entity)
       
   211         cdvalues = values.get(self.rname)
       
   212         if cdvalues is None:
       
   213             return self.current_values(entity)
       
   214         if not isinstance(cdvalues, (list, tuple)):
       
   215             cdvalues = (cdvalues,)
       
   216         return cdvalues
       
   217     
       
   218     def current_display_value(self, entity):
       
   219         """same as .current_value but consider values stored in session in case
       
   220         of validation error
       
   221         """
       
   222         return _value_from_values(self.current_display_values(entity))
       
   223     
       
   224     def hidden_input(self, entity, qvalue):
       
   225         """return an hidden field which
       
   226         1. indicates that a field is edited
       
   227         2. hold the old value to easily detect if the field has been modified
       
   228 
       
   229         `qvalue` is the html quoted old value
       
   230         """
       
   231         if self.role == 'subject':
       
   232             editmark = 'edits'
       
   233         else:
       
   234             editmark = 'edito'
       
   235         if qvalue is None or not entity.has_eid():
       
   236             qvalue = INTERNAL_FIELD_VALUE
       
   237         return u'<input type="hidden" name="%s-%s" value="%s"/>\n' % (
       
   238             editmark, self.rname, qvalue)
       
   239 
       
   240 class InputWidget(Widget):
       
   241     """abstract class for input generating a <input> tag"""
       
   242     input_type = None
       
   243     html_attributes = Widget.html_attributes | set(('type', 'name', 'value'))
       
   244 
       
   245     def _edit_render(self, entity):
       
   246         value = self.current_value(entity)
       
   247         dvalue = self.current_display_value(entity)
       
   248         if isinstance(value, basestring):
       
   249             value = html_escape(value)
       
   250         if isinstance(dvalue, basestring):
       
   251             dvalue = html_escape(dvalue)
       
   252         return u'%s<input type="%s" name="%s" value="%s" %s/>' % (
       
   253             self.hidden_input(entity, value), self.input_type,
       
   254             self.rname, dvalue, self.format_attrs())
       
   255 
       
   256 class HiddenWidget(InputWidget):
       
   257     input_type = 'hidden'
       
   258     autoid = False
       
   259     def __init__(self, vreg, subjschema, rschema, objschema,
       
   260                  role='subject', **kwattrs):
       
   261         InputWidget.__init__(self, vreg, subjschema, rschema, objschema,
       
   262                              role='subject', 
       
   263                              **kwattrs)
       
   264         # disable access key
       
   265         del self.attrs['accesskey']
       
   266 
       
   267     def current_value(self, entity):
       
   268         value = InputWidget.current_value(self, entity)
       
   269         return value or INTERNAL_FIELD_VALUE
       
   270 
       
   271     def current_display_value(self, entity):
       
   272         value = InputWidget.current_display_value(self, entity)
       
   273         return value or INTERNAL_FIELD_VALUE
       
   274     
       
   275     def render_label(self, entity, label=None):
       
   276         """render widget's label"""
       
   277         return u''
       
   278     
       
   279     def render_help(self, entity):
       
   280         return u''
       
   281     
       
   282     def hidden_input(self, entity, value):
       
   283         """no hidden input for hidden input"""
       
   284         return ''
       
   285     
       
   286 
       
   287 class EidWidget(HiddenWidget):
       
   288 
       
   289     def _edit_render(self, entity):
       
   290         return u'<input type="hidden" name="eid" value="%s" />' % entity.eid
       
   291 
       
   292 
       
   293 class StringWidget(InputWidget):
       
   294     input_type = 'text'
       
   295     html_attributes = InputWidget.html_attributes | set(('size', 'maxlength'))
       
   296     @staticmethod
       
   297     def size_constraint_attrs(attrs, maxsize):
       
   298         """set html attributes in the attrs dict to consider maxsize"""
       
   299         attrs['size'] = min(maxsize, 40)
       
   300         attrs['maxlength'] = maxsize
       
   301         
       
   302         
       
   303 class AutoCompletionWidget(StringWidget):
       
   304     cubicwebns_attributes = (StringWidget.cubicwebns_attributes |
       
   305                           set(('accesskey', 'size', 'maxlength')))
       
   306     attrs = ()
       
   307     
       
   308     wdgtype = 'SuggestField'
       
   309     
       
   310     def current_value(self, entity):
       
   311         value = StringWidget.current_value(self, entity)
       
   312         return value or INTERNAL_FIELD_VALUE
       
   313 
       
   314     def _get_url(self, entity):
       
   315         return entity.req.build_url('json', fname=entity.autocomplete_initfuncs[self.rschema],
       
   316                                 pageid=entity.req.pageid, mode='remote')
       
   317 
       
   318     def _edit_render(self, entity):
       
   319         req = entity.req
       
   320         req.add_js( ('cubicweb.widgets.js', 'jquery.autocomplete.js') )
       
   321         req.add_css('jquery.autocomplete.css')
       
   322         value = self.current_value(entity)
       
   323         dvalue = self.current_display_value(entity)
       
   324         if isinstance(value, basestring):
       
   325             value = html_escape(value)
       
   326         if isinstance(dvalue, basestring):
       
   327             dvalue = html_escape(dvalue)
       
   328         iid = self.attrs.pop('id')
       
   329         if self.required(entity):
       
   330             cssclass = u' required'
       
   331         else:
       
   332             cssclass = u''
       
   333         dataurl = self._get_url(entity)
       
   334         return (u'%(hidden)s<input type="text" name="%(iid)s" value="%(value)s" cubicweb:dataurl="%(url)s" class="widget%(required)s" id="%(iid)s" '
       
   335                 u'tabindex="%(tabindex)s" cubicweb:loadtype="auto" cubicweb:wdgtype="%(wdgtype)s"  %(attrs)s />' % {
       
   336                     'iid': iid,
       
   337                     'hidden': self.hidden_input(entity, value),
       
   338                     'wdgtype': self.wdgtype,
       
   339                     'url': html_escape(dataurl),
       
   340                     'tabindex': self.attrs.pop('tabindex'),
       
   341                     'value': dvalue,
       
   342                     'attrs': self.format_attrs(),
       
   343                     'required' : cssclass,
       
   344                     })
       
   345 
       
   346 class StaticFileAutoCompletionWidget(AutoCompletionWidget):
       
   347     wdgtype = 'StaticFileSuggestField'
       
   348     
       
   349     def _get_url(self, entity):
       
   350         return entity.req.datadir_url + entity.autocomplete_initfuncs[self.rschema]
       
   351 
       
   352 class RestrictedAutoCompletionWidget(AutoCompletionWidget):
       
   353     wdgtype = 'RestrictedSuggestField'    
       
   354 
       
   355     
       
   356 class PasswordWidget(InputWidget):
       
   357     input_type = 'password'
       
   358     
       
   359     def required(self, entity):
       
   360         if InputWidget.required(self, entity) and not entity.has_eid():
       
   361             return True
       
   362         return False
       
   363     
       
   364     def current_values(self, entity):
       
   365         # on existant entity, show password field has non empty (we don't have
       
   366         # the actual value
       
   367         if entity.has_eid():
       
   368             return (INTERNAL_FIELD_VALUE,)
       
   369         return super(PasswordWidget, self).current_values(entity)
       
   370 
       
   371     def _edit_render(self, entity):
       
   372         html = super(PasswordWidget, self)._edit_render(entity)
       
   373         name = eid_param(self.name + '-confirm', entity.eid)
       
   374         return u'%s<br/>\n<input type="%s" name="%s" id="%s" tabindex="%s"/>&nbsp;<span class="emphasis">(%s)</span>' % (
       
   375             html, self.input_type, name, name, entity.req.next_tabindex(),
       
   376             entity.req._('confirm password'))
       
   377 
       
   378     
       
   379 class TextWidget(Widget):
       
   380     html_attributes = Widget.html_attributes | set(('rows', 'cols'))
       
   381     
       
   382     @staticmethod
       
   383     def size_constraint_attrs(attrs, maxsize):
       
   384         """set html attributes in the attrs dict to consider maxsize"""
       
   385         if 256 < maxsize < 513:
       
   386             attrs['cols'], attrs['rows'] = 60, 5
       
   387         else:
       
   388             attrs['cols'], attrs['rows'] = 80, 10
       
   389     
       
   390     def render(self, entity):
       
   391         if not entity.has_eid():
       
   392             return u''
       
   393         return entity.printable_value(self.name)
       
   394     
       
   395     def add_fckeditor_info(self, req):
       
   396         req.add_js('fckeditor.js')
       
   397         req.fckeditor_config()
       
   398     
       
   399     def _edit_render(self, entity, with_format=True):
       
   400         req = entity.req
       
   401         editor = self._edit_render_textarea(entity, with_format)
       
   402         value = self.current_value(entity)
       
   403         if isinstance(value, basestring):
       
   404             value = html_escape(value)
       
   405         return u'%s%s' % (self.hidden_input(entity, value), editor)
       
   406     
       
   407     def _edit_render_textarea(self, entity, with_format):
       
   408         self.attrs.setdefault('cols', 80)
       
   409         self.attrs.setdefault('rows', 20)
       
   410         dvalue = self.current_display_value(entity)
       
   411         if isinstance(dvalue, basestring):
       
   412             dvalue = html_escape(dvalue)
       
   413         if entity.use_fckeditor(self.name):
       
   414             self.add_fckeditor_info(entity.req)
       
   415             if with_format:
       
   416                 if entity.has_eid():
       
   417                     format = entity.format(self.name)
       
   418                 else:
       
   419                     format = ''
       
   420                 frname = eid_param(self.name + '_format', entity.eid)
       
   421                 hidden = u'<input type="hidden" name="edits-%s" value="%s"/>\n'\
       
   422                          '<input type="hidden" name="%s" value="text/html"/>\n' % (
       
   423                     frname, format, frname)
       
   424             return u'%s<textarea cubicweb:type="wysiwyg" onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % (
       
   425                 hidden, self.rname, self.format_attrs(), dvalue)
       
   426         if with_format and entity.has_format(self.name):
       
   427             fmtwdg = entity.get_widget(self.name + '_format')
       
   428             fmtwdgstr = fmtwdg.edit_render(entity, tabindex=self.attrs['tabindex'])
       
   429             self.attrs['tabindex'] = entity.req.next_tabindex()
       
   430         else:
       
   431             fmtwdgstr = ''
       
   432         return u'%s<br/><textarea onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % (
       
   433             fmtwdgstr, self.rname, self.format_attrs(), dvalue)
       
   434             
       
   435     
       
   436 class CheckBoxWidget(Widget):
       
   437     html_attributes = Widget.html_attributes | set(('checked', ))
       
   438     def _edit_render(self, entity):
       
   439         value = self.current_value(entity)
       
   440         dvalue = self.current_display_value(entity)
       
   441         return self.hidden_input(entity, value) + checkbox(self.rname, 'checked', self.format_attrs(), dvalue)
       
   442 
       
   443     def render(self, entity):
       
   444         if not entity.has_eid():
       
   445             return u''
       
   446         if getattr(entity, self.name):
       
   447             return entity.req._('yes')
       
   448         return entity.req._('no')
       
   449 
       
   450 
       
   451 class YesNoRadioWidget(CheckBoxWidget):
       
   452     
       
   453     def _edit_render(self, entity):
       
   454         value = self.current_value(entity)
       
   455         dvalue = self.current_display_value(entity)
       
   456         attrs1 = self.format_attrs()
       
   457         del self.attrs['id'] # avoid duplicate id for xhtml compliance
       
   458         attrs2 = self.format_attrs()
       
   459         if dvalue:
       
   460             attrs1 += ' checked="checked"'
       
   461         else:
       
   462             attrs2 += ' checked="checked"'
       
   463         wdgs = [self.hidden_input(entity, value),
       
   464                 u'<input type="radio" name="%s" value="1" %s/>%s<br/>' % (self.rname, attrs1, entity.req._('yes')),
       
   465                 u'<input type="radio" name="%s" value="" %s/>%s<br/>' % (self.rname, attrs2, entity.req._('no'))]
       
   466         return '\n'.join(wdgs)
       
   467 
       
   468     
       
   469 class FileWidget(Widget):
       
   470     need_multipart = True
       
   471     def _file_wdg(self, entity):
       
   472         wdgs = [u'<input type="file" name="%s" %s/>' % (self.rname, self.format_attrs())]
       
   473         req = entity.req
       
   474         if entity.has_format(self.name) or entity.has_text_encoding(self.name):
       
   475             divid = '%s-%s-advanced' % (self.name, entity.eid)
       
   476             wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
       
   477                         (html_escape(toggle_action(divid)),
       
   478                          req._('show advanced fields'),
       
   479                          html_escape(req.build_url('data/puce_down.png')),
       
   480                          req._('show advanced fields')))
       
   481             wdgs.append(u'<div id="%s" class="hidden">' % divid)
       
   482             for extraattr in ('_format', '_encoding'):
       
   483                 if entity.e_schema.has_subject_relation('%s%s' % (self.name, extraattr)):
       
   484                     ewdg = entity.get_widget(self.name + extraattr)
       
   485                     wdgs.append(ewdg.render_label(entity))
       
   486                     wdgs.append(ewdg.edit_render(entity, includehelp=True))
       
   487                     wdgs.append(u'<br/>')
       
   488             wdgs.append(u'</div>')
       
   489         if entity.has_eid() and not self.required(entity):
       
   490             # trick to be able to delete an uploaded file
       
   491             wdgs.append(u'<br/>')
       
   492             wdgs.append(checkbox(eid_param('__%s_detach' % self.rname, entity.eid), False))
       
   493             wdgs.append(req._('detach attached file'))
       
   494         return '\n'.join(wdgs)
       
   495     
       
   496     def _edit_render(self, entity):
       
   497         return self.hidden_input(entity, None) + self._file_wdg(entity)
       
   498 
       
   499 
       
   500 class TextFileWidget(FileWidget):
       
   501     def _edit_msg(self, entity):
       
   502         if entity.has_eid() and not self.required(entity):
       
   503             msg = entity.req._(
       
   504                 'You can either submit a new file using the browse button above'
       
   505                 ', or choose to remove already uploaded file by checking the '
       
   506                 '"detach attached file" check-box, or edit file content online '
       
   507                 'with the widget below.')
       
   508         else:
       
   509             msg = entity.req._(
       
   510                 'You can either submit a new file using the browse button above'
       
   511                 ', or edit file content online with the widget below.')
       
   512         return msg
       
   513     
       
   514     def _edit_render(self, entity):
       
   515         wdgs = [self._file_wdg(entity)]
       
   516         if entity.format(self.name) in ('text/plain', 'text/html', 'text/rest'):
       
   517             msg = self._edit_msg(entity)
       
   518             wdgs.append(u'<p><b>%s</b></p>' % msg)
       
   519             twdg = TextWidget(self.vreg, self.subjtype, self.rschema, self.objtype)
       
   520             twdg.rname = self.rname
       
   521             data = getattr(entity, self.name)
       
   522             if data:
       
   523                 encoding = entity.text_encoding(self.name)
       
   524                 try:
       
   525                     entity[self.name] = unicode(data.getvalue(), encoding)
       
   526                 except UnicodeError:
       
   527                     pass
       
   528                 else:
       
   529                     wdgs.append(twdg.edit_render(entity, with_format=False))
       
   530                     entity[self.name] = data # restore Binary value
       
   531             wdgs.append(u'<br/>')
       
   532         return '\n'.join(wdgs)
       
   533 
       
   534 
       
   535 class ComboBoxWidget(Widget):
       
   536     html_attributes = Widget.html_attributes | set(('multiple', 'size'))
       
   537     
       
   538     def __init__(self, vreg, subjschema, rschema, objschema,
       
   539                  multiple=False, **kwattrs):
       
   540         super(ComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
       
   541                                              **kwattrs)
       
   542         if multiple:
       
   543             self.attrs['multiple'] = 'multiple'
       
   544             if not 'size' in self.attrs:
       
   545                 self.attrs['size'] = '5'
       
   546         # disable access key (dunno why but this is not allowed by xhtml 1.0)
       
   547         del self.attrs['accesskey']
       
   548         
       
   549     def vocabulary(self, entity):
       
   550         raise NotImplementedError()
       
   551     
       
   552     def form_value(self, entity, value, values):
       
   553         if value in values:
       
   554             flag = 'selected="selected"'
       
   555         else:
       
   556             flag = ''
       
   557         return value, flag
       
   558 
       
   559     def _edit_render(self, entity):
       
   560         values = self.current_values(entity)
       
   561         if values:
       
   562             res = [self.hidden_input(entity, v) for v in values]
       
   563         else:
       
   564             res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
       
   565         dvalues = self.current_display_values(entity)
       
   566         res.append(u'<select name="%s" %s>' % (self.rname, self.format_attrs()))
       
   567         for label, value in self.vocabulary(entity):
       
   568             if value is None:
       
   569                 # handle separator
       
   570                 res.append(u'<optgroup label="%s"/>' % (label or ''))
       
   571             else:
       
   572                 value, flag = self.form_value(entity, value, dvalues)
       
   573                 res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label)))
       
   574         res.append(u'</select>')
       
   575         return '\n'.join(res)
       
   576 
       
   577  
       
   578 class StaticComboBoxWidget(ComboBoxWidget):
       
   579     
       
   580     def __init__(self, vreg, subjschema, rschema, objschema,
       
   581                  vocabfunc, multiple=False, sort=False, **kwattrs):
       
   582         super(StaticComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
       
   583                                                    multiple, **kwattrs)
       
   584         self.sort = sort
       
   585         self.vocabfunc = vocabfunc
       
   586 
       
   587     def vocabulary(self, entity):
       
   588         choices = self.vocabfunc(entity)
       
   589         if self.sort:
       
   590             choices = sorted(choices)
       
   591         if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'):
       
   592             return zip((entity.req._(v) for v in choices), choices)
       
   593         return zip(choices, choices)
       
   594     
       
   595 
       
   596 class EntityLinkComboBoxWidget(ComboBoxWidget):
       
   597     """to be used be specific forms"""
       
   598     
       
   599     def current_values(self, entity):
       
   600         if entity.has_eid():
       
   601             return [r[0] for r in entity.related(self.name, self.role)]
       
   602         defaultmeth = 'default_%s_%s' % (self.role, self.name)
       
   603         if hasattr(entity, defaultmeth):
       
   604             return getattr(entity, defaultmeth)()
       
   605         return ()
       
   606     
       
   607     def vocabulary(self, entity):
       
   608         return [('', INTERNAL_FIELD_VALUE)] + entity.vocabulary(self.rschema, self.role)
       
   609 
       
   610 
       
   611 class RawDynamicComboBoxWidget(EntityLinkComboBoxWidget):
       
   612     
       
   613     def vocabulary(self, entity, limit=None):
       
   614         req = entity.req
       
   615         # first see if its specified by __linkto form parameters
       
   616         linkedto = entity.linked_to(self.name, self.role)
       
   617         if linkedto:
       
   618             entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto)
       
   619             return [(entity.view('combobox'), entity.eid) for entity in entities]
       
   620         # it isn't, check if the entity provides a method to get correct values
       
   621         if not self.required(entity):
       
   622             res = [('', INTERNAL_FIELD_VALUE)]
       
   623         else:
       
   624             res = []
       
   625         # vocabulary doesn't include current values, add them
       
   626         if entity.has_eid():
       
   627             rset = entity.related(self.name, self.role)
       
   628             relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
       
   629         else:
       
   630             relatedvocab = []
       
   631         return res + entity.vocabulary(self.rschema, self.role) + relatedvocab
       
   632 
       
   633 
       
   634 class DynamicComboBoxWidget(RawDynamicComboBoxWidget):
       
   635     
       
   636     def vocabulary(self, entity, limit=None):
       
   637         return sorted(super(DynamicComboBoxWidget, self).vocabulary(entity, limit))
       
   638 
       
   639 
       
   640 class AddComboBoxWidget(DynamicComboBoxWidget):
       
   641     def _edit_render(self, entity):
       
   642         req = entity.req
       
   643         req.add_js( ('cubicweb.ajax.js', 'jquery.js', 'cubicweb.widgets.js') )
       
   644         values = self.current_values(entity)
       
   645         if values:
       
   646             res = [self.hidden_input(entity, v) for v in values]
       
   647         else:
       
   648             res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
       
   649         dvalues = self.current_display_values(entity)
       
   650         etype_from = entity.e_schema.subject_relation(self.name).objects(entity.e_schema)[0]
       
   651         res.append(u'<select class="widget" cubicweb:etype_to="%s" cubicweb:etype_from="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="AddComboBox" name="%s" %s>'
       
   652                    % (entity.e_schema, etype_from, self.rname, self.format_attrs()))
       
   653         for label, value in self.vocabulary(entity):
       
   654             if value is None:
       
   655                 # handle separator
       
   656                 res.append(u'<optgroup label="%s"/>' % (label or ''))
       
   657             else:
       
   658                 value, flag = self.form_value(entity, value, dvalues)
       
   659                 res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label)))
       
   660         res.append(u'</select>')
       
   661         res.append(u'<div id="newvalue">')
       
   662         res.append(u'<input type="text" id="newopt" />')
       
   663         res.append(u'<a href="javascript:noop()" id="add_newopt">&nbsp;</a></div>')
       
   664         return '\n'.join(res)
       
   665 
       
   666 class IntegerWidget(StringWidget):
       
   667     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   668         kwattrs['size'] = 5
       
   669         kwattrs['maxlength'] = 15
       
   670         StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   671         
       
   672     def render_example(self, req):
       
   673         return '23'
       
   674     
       
   675 
       
   676         
       
   677 class FloatWidget(StringWidget):
       
   678     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   679         kwattrs['size'] = 5
       
   680         kwattrs['maxlength'] = 15
       
   681         StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   682 
       
   683     def render_example(self, req):
       
   684         formatstr = req.property_value('ui.float-format')
       
   685         return formatstr % 1.23
       
   686     
       
   687     def current_values(self, entity):
       
   688         values = entity.attribute_values(self.name)
       
   689         if values:
       
   690             formatstr = entity.req.property_value('ui.float-format')
       
   691             value = values[0]
       
   692             if value is not None:
       
   693                 value = float(value)
       
   694             else:
       
   695                 return ()
       
   696             return [formatstr % value]
       
   697         return ()
       
   698 
       
   699 class DecimalWidget(StringWidget):
       
   700     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   701         kwattrs['size'] = 5
       
   702         kwattrs['maxlength'] = 15
       
   703         StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   704         
       
   705     def render_example(self, req):
       
   706         return '345.0300'
       
   707     
       
   708 
       
   709 
       
   710 class DateWidget(StringWidget):
       
   711     format_key = 'ui.date-format'
       
   712     monthnames = ("january", "february", "march", "april",
       
   713                   "may", "june", "july", "august",
       
   714                   "september", "october", "november", "december")
       
   715     
       
   716     daynames = ("monday", "tuesday", "wednesday", "thursday",
       
   717                 "friday", "saturday", "sunday")
       
   718     
       
   719     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   720         kwattrs.setdefault('size', 10)
       
   721         kwattrs.setdefault('maxlength', 10)
       
   722         StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   723 
       
   724     def current_values(self, entity):
       
   725         values = entity.attribute_values(self.name)
       
   726         if values and hasattr(values[0], 'strftime'):
       
   727             formatstr = entity.req.property_value(self.format_key)
       
   728             return [values[0].strftime(formatstr)]
       
   729         return values
       
   730 
       
   731     def render_example(self, req):
       
   732         formatstr = req.property_value(self.format_key)
       
   733         return now().strftime(formatstr)
       
   734 
       
   735     def add_localized_infos(self, req):
       
   736         """inserts JS variables defining localized months and days"""
       
   737         # import here to avoid dependancy from cubicweb-common to simplejson
       
   738         _ = req._
       
   739         monthnames = [_(mname) for mname in self.monthnames]
       
   740         daynames = [_(dname) for dname in self.daynames]
       
   741         req.html_headers.define_var('MONTHNAMES', monthnames)
       
   742         req.html_headers.define_var('DAYNAMES', daynames)
       
   743 
       
   744 
       
   745     def _edit_render(self, entity):
       
   746         wdg = super(DateWidget, self)._edit_render(entity)
       
   747         cal_button = self.render_calendar_popup(entity)
       
   748         return wdg+cal_button
       
   749 
       
   750     def render_help(self, entity):
       
   751         """calendar popup widget"""
       
   752         req = entity.req
       
   753         help = [ u'<br/>' ]
       
   754         descr = self.rschema.rproperty(self.subjtype, self.objtype, 'description')
       
   755         if descr:
       
   756             help.append('<span class="helper">%s</span>' % req._(descr))
       
   757         example = self.render_example(req)
       
   758         if example:
       
   759             help.append('<span class="helper">(%s: %s)</span>'
       
   760                         % (req._('sample format'), example))
       
   761         return u'&nbsp;'.join(help)
       
   762 
       
   763     def render_calendar_popup(self, entity):
       
   764         """calendar popup widget"""
       
   765         req = entity.req
       
   766         self.add_localized_infos(req)
       
   767         req.add_js(('cubicweb.ajax.js', 'cubicweb.calendar.js',))
       
   768         req.add_css(('cubicweb.calendar_popup.css',))
       
   769         inputid = self.attrs.get('id', self.rname)
       
   770         helperid = "%shelper" % inputid
       
   771         _today = today()
       
   772         year = int(req.form.get('year', _today.year))
       
   773         month = int(req.form.get('month', _today.month))
       
   774 
       
   775         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
       
   776 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
       
   777                 % (helperid, inputid, year, month,
       
   778                    req.external_resource('CALENDAR_ICON'), req._('calendar'), helperid) )
       
   779 
       
   780 class DateTimeWidget(DateWidget):
       
   781     format_key = 'ui.datetime-format'
       
   782     
       
   783     def render_example(self, req):
       
   784         formatstr1 = req.property_value('ui.datetime-format')
       
   785         formatstr2 = req.property_value('ui.date-format')
       
   786         return req._('%s, or without time: %s') % (now().strftime(formatstr1),
       
   787                                                    now().strftime(formatstr2))
       
   788 
       
   789 
       
   790 
       
   791 
       
   792     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   793         kwattrs['size'] = 16
       
   794         kwattrs['maxlength'] = 16
       
   795         DateWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   796 
       
   797 
       
   798 class TimeWidget(StringWidget):
       
   799     format_key = 'ui.time-format'
       
   800     def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
       
   801         kwattrs['size'] = 5
       
   802         kwattrs['maxlength'] = 5
       
   803         StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
       
   804 
       
   805         
       
   806 class EmailWidget(StringWidget):
       
   807     
       
   808     def render(self, entity):
       
   809         email = getattr(entity, self.name)
       
   810         if not email:
       
   811             return u''
       
   812         return u'<a href="mailto:%s">%s</a>' % (email, email)
       
   813         
       
   814 class URLWidget(StringWidget):
       
   815     
       
   816     def render(self, entity):
       
   817         url = getattr(entity, self.name)
       
   818         if not url:
       
   819             return u''
       
   820         url = html_escape(url)
       
   821         return u'<a href="%s">%s</a>' % (url, url)
       
   822     
       
   823 class EmbededURLWidget(StringWidget):
       
   824     
       
   825     def render(self, entity):
       
   826         url = getattr(entity, self.name)
       
   827         if not url:
       
   828             return u''
       
   829         aurl = html_escape(entity.build_url('embed', url=url))
       
   830         return u'<a href="%s">%s</a>' % (aurl, url)
       
   831 
       
   832 
       
   833 
       
   834 class PropertyKeyWidget(ComboBoxWidget):
       
   835     """specific widget for EProperty.pkey field to set the value widget according to
       
   836     the selected key
       
   837     """
       
   838     
       
   839     def _edit_render(self, entity):
       
   840         entity.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
       
   841         vtabindex = self.attrs.get('tabindex', 0) + 1
       
   842         self.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % (
       
   843             entity.eid, vtabindex)
       
   844         # limit size
       
   845         if not entity.has_eid():
       
   846             self.attrs['size'] = 10
       
   847         else:
       
   848             self.attrs['size'] = 1
       
   849         return super(PropertyKeyWidget, self)._edit_render(entity)
       
   850     
       
   851     def vocabulary(self, entity):
       
   852         _ = entity.req._
       
   853         if entity.has_eid():
       
   854             return [(_(entity.pkey), entity.pkey)]
       
   855         # key beginning with 'system.' should usually not be edited by hand
       
   856         choices = entity.vreg.user_property_keys()
       
   857         return sorted(zip((_(v) for v in choices), choices))
       
   858 
       
   859 
       
   860 class PropertyValueWidget(Widget):
       
   861     """specific widget for EProperty.value field which will be different according to
       
   862     the selected key type and vocabulary information
       
   863     """
       
   864     
       
   865     def render_help(self, entity):
       
   866         return u''
       
   867         
       
   868     def render(self, entity):
       
   869         assert entity.has_eid()
       
   870         w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs)
       
   871         return w.render(entity)
       
   872         
       
   873     def _edit_render(self, entity):
       
   874         if not entity.has_eid():
       
   875             # no key set yet, just include an empty div which will be filled
       
   876             # on key selection
       
   877             # empty span as well else html validation fail (label is refering to this id)
       
   878             return u'<div id="div:%s"><span id="%s"/></div>' % (self.rname, self.attrs.get('id'))
       
   879         w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs)
       
   880         if entity.pkey.startswith('system.'):
       
   881             value = '<span class="value" id="%s">%s</span>' % (self.attrs.get('id'), w.render(entity))
       
   882             msg = entity.req._('value associated to this key is not editable manually')
       
   883             return value + '<div>%s</div>' % msg
       
   884         return w.edit_render(entity, self.attrs.get('tabindex'), includehelp=True)
       
   885     
       
   886 
       
   887 def widget_factory(vreg, subjschema, rschema, objschema, role='subject',
       
   888                    **kwargs):
       
   889     """return the most adapated widget to edit the relation
       
   890     'subjschema rschema objschema' according to information found in the schema
       
   891     """
       
   892     if role == 'subject':
       
   893         eclass, subjschema = _eclass_eschema(subjschema)
       
   894     else:
       
   895         eclass, objschema = _eclass_eschema(objschema)
       
   896     if eclass is not None and rschema in eclass.widgets:
       
   897         wcls = WIDGETS[eclass.widgets[rschema]]
       
   898     elif not rschema.is_final():
       
   899         card = rschema.rproperty(subjschema, objschema, 'cardinality')
       
   900         if role == 'object':
       
   901             multiple = card[1] in '+*'
       
   902         else: #if role == 'subject':
       
   903             multiple = card[0] in '+*'
       
   904         return DynamicComboBoxWidget(vreg, subjschema, rschema, objschema,
       
   905                                      role=role, multiple=multiple)
       
   906     else:
       
   907         wcls = None
       
   908     factory = FACTORIES.get(objschema, _default_widget_factory)
       
   909     return factory(vreg, subjschema, rschema, objschema, wcls=wcls,
       
   910                    role=role, **kwargs)
       
   911 
       
   912 
       
   913 # factories to find the most adapated widget according to a type and other constraints
       
   914                 
       
   915 def _string_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
       
   916     w = None
       
   917     for c in rschema.rproperty(subjschema, objschema, 'constraints'):
       
   918         if isinstance(c, StaticVocabularyConstraint):
       
   919             # may have been set by a previous SizeConstraint but doesn't make sense
       
   920             # here (even doesn't have the same meaning on a combobox actually)
       
   921             kwargs.pop('size', None) 
       
   922             return (wcls or StaticComboBoxWidget)(vreg, subjschema, rschema, objschema,
       
   923                                                   vocabfunc=c.vocabulary, **kwargs)
       
   924         if isinstance(c, SizeConstraint) and c.max is not None:
       
   925             # don't return here since a StaticVocabularyConstraint may
       
   926             # follow
       
   927             if wcls is None:
       
   928                 if c.max < 257:
       
   929                     _wcls = StringWidget
       
   930                 else:
       
   931                     _wcls = TextWidget
       
   932             else:
       
   933                 _wcls = wcls
       
   934             _wcls.size_constraint_attrs(kwargs, c.max)
       
   935             w = _wcls(vreg, subjschema, rschema, objschema, **kwargs)
       
   936     if w is None:
       
   937         w = (wcls or TextWidget)(vreg, subjschema, rschema, objschema, **kwargs)
       
   938     return w
       
   939 
       
   940 def _default_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
       
   941     if wcls is None:
       
   942         wcls = _WFACTORIES[objschema]
       
   943     return wcls(vreg, subjschema, rschema, objschema, **kwargs)
       
   944 
       
   945 FACTORIES = {
       
   946     'String' :  _string_widget_factory,
       
   947     'Boolean':  _default_widget_factory,
       
   948     'Bytes':    _default_widget_factory,
       
   949     'Date':     _default_widget_factory,
       
   950     'Datetime': _default_widget_factory,
       
   951     'Float':    _default_widget_factory,
       
   952     'Decimal':    _default_widget_factory,
       
   953     'Int':      _default_widget_factory,
       
   954     'Password': _default_widget_factory,
       
   955     'Time':     _default_widget_factory,
       
   956     }
       
   957 
       
   958 # default widget by entity's type
       
   959 _WFACTORIES = {
       
   960     'Boolean':  YesNoRadioWidget,
       
   961     'Bytes':    FileWidget,
       
   962     'Date':     DateWidget,
       
   963     'Datetime': DateTimeWidget,
       
   964     'Int':      IntegerWidget,
       
   965     'Float':    FloatWidget,
       
   966     'Decimal':  DecimalWidget,
       
   967     'Password': PasswordWidget,
       
   968     'String' :  StringWidget,
       
   969     'Time':     TimeWidget,
       
   970     }
       
   971     
       
   972 # widgets registry
       
   973 WIDGETS = {}
       
   974 def register(widget_list):
       
   975     for obj in widget_list:
       
   976         if isinstance(obj, type) and issubclass(obj, Widget):
       
   977             if obj is Widget or obj is ComboBoxWidget:
       
   978                 continue
       
   979             WIDGETS[obj.__name__] = obj
       
   980 
       
   981 register(globals().values())