web/formwidgets.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """
       
    19 Widgets
       
    20 ~~~~~~~
       
    21 
       
    22 .. Note::
       
    23    A widget is responsible for the display of a field. It may use more than one
       
    24    HTML input tags. When the form is posted, a widget is also reponsible to give
       
    25    back to the field something it can understand.
       
    26 
       
    27    Of course you can not use any widget with any field...
       
    28 
       
    29 .. autoclass:: cubicweb.web.formwidgets.FieldWidget
       
    30 
       
    31 
       
    32 HTML <input> based widgets
       
    33 ''''''''''''''''''''''''''
       
    34 
       
    35 .. autoclass:: cubicweb.web.formwidgets.HiddenInput
       
    36 .. autoclass:: cubicweb.web.formwidgets.TextInput
       
    37 .. autoclass:: cubicweb.web.formwidgets.EmailInput
       
    38 .. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput
       
    39 .. autoclass:: cubicweb.web.formwidgets.FileInput
       
    40 .. autoclass:: cubicweb.web.formwidgets.ButtonInput
       
    41 
       
    42 
       
    43 Other standard HTML widgets
       
    44 '''''''''''''''''''''''''''
       
    45 
       
    46 .. autoclass:: cubicweb.web.formwidgets.TextArea
       
    47 .. autoclass:: cubicweb.web.formwidgets.Select
       
    48 .. autoclass:: cubicweb.web.formwidgets.CheckBox
       
    49 .. autoclass:: cubicweb.web.formwidgets.Radio
       
    50 
       
    51 
       
    52 Date and time widgets
       
    53 '''''''''''''''''''''
       
    54 
       
    55 .. autoclass:: cubicweb.web.formwidgets.DateTimePicker
       
    56 .. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker
       
    57 .. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker
       
    58 .. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker
       
    59 
       
    60 
       
    61 Ajax / javascript widgets
       
    62 '''''''''''''''''''''''''
       
    63 
       
    64 .. autoclass:: cubicweb.web.formwidgets.FCKEditor
       
    65 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget
       
    66 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
       
    67 .. autoclass:: cubicweb.web.formwidgets.InOutWidget
       
    68 
       
    69 .. kill or document StaticFileAutoCompletionWidget
       
    70 .. kill or document LazyRestrictedAutoCompletionWidget
       
    71 .. kill or document RestrictedAutoCompletionWidget
       
    72 
       
    73 
       
    74 Other widgets
       
    75 '''''''''''''
       
    76 
       
    77 .. autoclass:: cubicweb.web.formwidgets.PasswordInput
       
    78 .. autoclass:: cubicweb.web.formwidgets.IntervalWidget
       
    79 .. autoclass:: cubicweb.web.formwidgets.BitSelect
       
    80 .. autoclass:: cubicweb.web.formwidgets.HorizontalLayoutWidget
       
    81 .. autoclass:: cubicweb.web.formwidgets.EditableURLWidget
       
    82 
       
    83 
       
    84 Form controls
       
    85 '''''''''''''
       
    86 
       
    87 Those classes are not proper widget (they are not associated to field) but are
       
    88 used as form controls. Their API is similar to widgets except that `field`
       
    89 argument given to :meth:`render` will be `None`.
       
    90 
       
    91 .. autoclass:: cubicweb.web.formwidgets.Button
       
    92 .. autoclass:: cubicweb.web.formwidgets.SubmitButton
       
    93 .. autoclass:: cubicweb.web.formwidgets.ResetButton
       
    94 .. autoclass:: cubicweb.web.formwidgets.ImgButton
       
    95 """
       
    96 __docformat__ = "restructuredtext en"
       
    97 
       
    98 from functools import reduce
       
    99 from datetime import date
       
   100 
       
   101 from six import text_type, string_types
       
   102 
       
   103 from logilab.mtconverter import xml_escape
       
   104 from logilab.common.date import todatetime
       
   105 
       
   106 from cubicweb import tags, uilib
       
   107 from cubicweb.utils import json_dumps
       
   108 from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE, ProcessFormError
       
   109 
       
   110 
       
   111 class FieldWidget(object):
       
   112     """The abstract base class for widgets.
       
   113 
       
   114     **Attributes**
       
   115 
       
   116     Here are standard attributes of a widget, that may be set on concrete class
       
   117     to override default behaviours:
       
   118 
       
   119     :attr:`needs_js`
       
   120        list of javascript files needed by the widget.
       
   121 
       
   122     :attr:`needs_css`
       
   123        list of css files needed by the widget.
       
   124 
       
   125     :attr:`setdomid`
       
   126        flag telling if HTML DOM identifier should be set on input.
       
   127 
       
   128     :attr:`settabindex`
       
   129        flag telling if HTML tabindex attribute of inputs should be set.
       
   130 
       
   131     :attr:`suffix`
       
   132        string to use a suffix when generating input, to ease usage as a
       
   133        sub-widgets (eg widget used by another widget)
       
   134 
       
   135     :attr:`vocabulary_widget`
       
   136        flag telling if this widget expect a vocabulary
       
   137 
       
   138     Also, widget instances takes as first argument a `attrs` dictionary which
       
   139     will be stored in the attribute of the same name. It contains HTML
       
   140     attributes that should be set in the widget's input tag (though concrete
       
   141     classes may ignore it).
       
   142 
       
   143     .. currentmodule:: cubicweb.web.formwidgets
       
   144 
       
   145     **Form generation methods**
       
   146 
       
   147     .. automethod:: render
       
   148     .. automethod:: _render
       
   149     .. automethod:: values
       
   150     .. automethod:: attributes
       
   151 
       
   152     **Post handling methods**
       
   153 
       
   154     .. automethod:: process_field_data
       
   155 
       
   156     """
       
   157     needs_js = ()
       
   158     needs_css = ()
       
   159     setdomid = True
       
   160     settabindex = True
       
   161     suffix = None
       
   162     # does this widget expect a vocabulary
       
   163     vocabulary_widget = False
       
   164 
       
   165     def __init__(self, attrs=None, setdomid=None, settabindex=None, suffix=None):
       
   166         if attrs is None:
       
   167             attrs = {}
       
   168         self.attrs = attrs
       
   169         if setdomid is not None:
       
   170             # override class's default value
       
   171             self.setdomid = setdomid
       
   172         if settabindex is not None:
       
   173             # override class's default value
       
   174             self.settabindex = settabindex
       
   175         if suffix is not None:
       
   176             self.suffix = suffix
       
   177 
       
   178     def add_media(self, form):
       
   179         """adds media (CSS & JS) required by this widget"""
       
   180         if self.needs_js:
       
   181             form._cw.add_js(self.needs_js)
       
   182         if self.needs_css:
       
   183             form._cw.add_css(self.needs_css)
       
   184 
       
   185     def render(self, form, field, renderer=None):
       
   186         """Called to render the widget for the given `field` in the given
       
   187         `form`.  Return a unicode string containing the HTML snippet.
       
   188 
       
   189         You will usually prefer to override the :meth:`_render` method so you
       
   190         don't have to handle addition of needed javascript / css files.
       
   191         """
       
   192         self.add_media(form)
       
   193         return self._render(form, field, renderer)
       
   194 
       
   195     def _render(self, form, field, renderer):
       
   196         """This is the method you have to implement in concrete widget classes.
       
   197         """
       
   198         raise NotImplementedError()
       
   199 
       
   200     def format_value(self, form, field, value):
       
   201         return field.format_value(form._cw, value)
       
   202 
       
   203     def attributes(self, form, field):
       
   204         """Return HTML attributes for the widget, automatically setting DOM
       
   205         identifier and tabindex when desired (see :attr:`setdomid` and
       
   206         :attr:`settabindex` attributes)
       
   207         """
       
   208         attrs = dict(self.attrs)
       
   209         if self.setdomid:
       
   210             attrs['id'] = field.dom_id(form, self.suffix)
       
   211         if self.settabindex and 'tabindex' not in attrs:
       
   212             attrs['tabindex'] = form._cw.next_tabindex()
       
   213         if 'placeholder' in attrs:
       
   214             attrs['placeholder'] = form._cw._(attrs['placeholder'])
       
   215         return attrs
       
   216 
       
   217     def values(self, form, field):
       
   218         """Return the current *string* values (i.e. for display in an HTML
       
   219         string) for the given field. This method returns a list of values since
       
   220         it's suitable for all kind of widgets, some of them taking multiple
       
   221         values, but you'll get a single value in the list in most cases.
       
   222 
       
   223         Those values are searched in:
       
   224 
       
   225         1. previously submitted form values if any (on validation error)
       
   226 
       
   227         2. req.form (specified using request parameters)
       
   228 
       
   229         3. extra form values given to form.render call (specified the code
       
   230            generating the form)
       
   231 
       
   232         4. field's typed value (returned by its
       
   233            :meth:`~cubicweb.web.formfields.Field.typed_value` method)
       
   234 
       
   235         Values found in 1. and 2. are expected te be already some 'display
       
   236         value' (eg a string) while those found in 3. and 4. are expected to be
       
   237         correctly typed value.
       
   238 
       
   239         3 and 4 are handle by the :meth:`typed_value` method to ease reuse in
       
   240         concrete classes.
       
   241         """
       
   242         values = None
       
   243         if not field.ignore_req_params:
       
   244             qname = field.input_name(form, self.suffix)
       
   245             # value from a previous post that has raised a validation error
       
   246             if qname in form.form_previous_values:
       
   247                 values = form.form_previous_values[qname]
       
   248             # value specified using form parameters
       
   249             elif qname in form._cw.form:
       
   250                 values = form._cw.form[qname]
       
   251             elif field.name != qname and field.name in form._cw.form:
       
   252                 # XXX compat: accept attr=value in req.form to specify value of
       
   253                 # attr-subject
       
   254                 values = form._cw.form[field.name]
       
   255         if values is None:
       
   256             values = self.typed_value(form, field)
       
   257             if values != INTERNAL_FIELD_VALUE:
       
   258                 values = self.format_value(form, field, values)
       
   259         if not isinstance(values, (tuple, list)):
       
   260             values = (values,)
       
   261         return values
       
   262 
       
   263     def typed_value(self, form, field):
       
   264         """return field's *typed* value specified in:
       
   265         3. extra form values given to render()
       
   266         4. field's typed value
       
   267         """
       
   268         qname = field.input_name(form)
       
   269         for key in ((field, form), qname):
       
   270             try:
       
   271                 return form.formvalues[key]
       
   272             except KeyError:
       
   273                 continue
       
   274         if field.name != qname and field.name in form.formvalues:
       
   275             return form.formvalues[field.name]
       
   276         return field.typed_value(form)
       
   277 
       
   278     def process_field_data(self, form, field):
       
   279         """Return process posted value(s) for widget and return something
       
   280         understandable by the associated `field`. That value may be correctly
       
   281         typed or a string that the field may parse.
       
   282         """
       
   283         posted = form._cw.form
       
   284         val = posted.get(field.input_name(form, self.suffix))
       
   285         if isinstance(val, string_types):
       
   286             val = val.strip()
       
   287         return val
       
   288 
       
   289     # XXX deprecates
       
   290     def values_and_attributes(self, form, field):
       
   291         return self.values(form, field), self.attributes(form, field)
       
   292 
       
   293 
       
   294 class Input(FieldWidget):
       
   295     """abstract widget class for <input> tag based widgets"""
       
   296     type = None
       
   297 
       
   298     def _render(self, form, field, renderer):
       
   299         """render the widget for the given `field` of `form`.
       
   300 
       
   301         Generate one <input> tag for each field's value
       
   302         """
       
   303         values, attrs = self.values_and_attributes(form, field)
       
   304         # ensure something is rendered
       
   305         if not values:
       
   306             values = (INTERNAL_FIELD_VALUE,)
       
   307         inputs = [tags.input(name=field.input_name(form, self.suffix),
       
   308                              type=self.type, value=value, **attrs)
       
   309                   for value in values]
       
   310         return u'\n'.join(inputs)
       
   311 
       
   312 
       
   313 # basic html widgets ###########################################################
       
   314 
       
   315 class TextInput(Input):
       
   316     """Simple <input type='text'>, will return a unicode string."""
       
   317     type = 'text'
       
   318 
       
   319 
       
   320 class EmailInput(Input):
       
   321     """Simple <input type='email'>, will return a unicode string."""
       
   322     type = 'email'
       
   323 
       
   324 
       
   325 class PasswordSingleInput(Input):
       
   326     """Simple <input type='password'>, will return a utf-8 encoded string.
       
   327 
       
   328     You may prefer using the :class:`~cubicweb.web.formwidgets.PasswordInput`
       
   329     widget which handles password confirmation.
       
   330     """
       
   331     type = 'password'
       
   332 
       
   333     def process_field_data(self, form, field):
       
   334         value = super(PasswordSingleInput, self).process_field_data(form, field)
       
   335         if value is not None:
       
   336             return value.encode('utf-8')
       
   337         return value
       
   338 
       
   339 
       
   340 class PasswordInput(Input):
       
   341     """<input type='password'> and a confirmation input. Form processing will
       
   342     fail if password and confirmation differs, else it will return the password
       
   343     as a utf-8 encoded string.
       
   344     """
       
   345     type = 'password'
       
   346 
       
   347     def _render(self, form, field, renderer):
       
   348         assert self.suffix is None, 'suffix not supported'
       
   349         values, attrs = self.values_and_attributes(form, field)
       
   350         assert len(values) == 1
       
   351         domid = attrs.pop('id')
       
   352         inputs = [tags.input(name=field.input_name(form),
       
   353                              value=values[0], type=self.type, id=domid, **attrs),
       
   354                   '<br/>',
       
   355                   tags.input(name=field.input_name(form, '-confirm'),
       
   356                              value=values[0], type=self.type, **attrs),
       
   357                   '&#160;', tags.span(form._cw._('confirm password'),
       
   358                                       **{'class': 'emphasis'})]
       
   359         return u'\n'.join(inputs)
       
   360 
       
   361     def process_field_data(self, form, field):
       
   362         passwd1 = super(PasswordInput, self).process_field_data(form, field)
       
   363         passwd2 = form._cw.form.get(field.input_name(form, '-confirm'))
       
   364         if passwd1 == passwd2:
       
   365             if passwd1 is None:
       
   366                 return None
       
   367             return passwd1.encode('utf-8')
       
   368         raise ProcessFormError(form._cw._("password and confirmation don't match"))
       
   369 
       
   370 
       
   371 class FileInput(Input):
       
   372     """Simple <input type='file'>, will return a tuple (name, stream) where
       
   373     name is the posted file name and stream a file like object containing the
       
   374     posted file data.
       
   375     """
       
   376     type = 'file'
       
   377 
       
   378     def values(self, form, field):
       
   379         # ignore value which makes no sense here (XXX even on form validation error?)
       
   380         return ('',)
       
   381 
       
   382 
       
   383 class HiddenInput(Input):
       
   384     """Simple <input type='hidden'> for hidden value, will return a unicode
       
   385     string.
       
   386     """
       
   387     type = 'hidden'
       
   388     setdomid = False  # by default, don't set id attribute on hidden input
       
   389     settabindex = False
       
   390 
       
   391 
       
   392 class ButtonInput(Input):
       
   393     """Simple <input type='button'>, will return a unicode string.
       
   394 
       
   395     If you want a global form button, look at the :class:`Button`,
       
   396     :class:`SubmitButton`, :class:`ResetButton` and :class:`ImgButton` below.
       
   397     """
       
   398     type = 'button'
       
   399 
       
   400 
       
   401 class TextArea(FieldWidget):
       
   402     """Simple <textarea>, will return a unicode string."""
       
   403     _minrows = 2
       
   404     _maxrows = 15
       
   405     _columns = 80
       
   406 
       
   407     def _render(self, form, field, renderer):
       
   408         values, attrs = self.values_and_attributes(form, field)
       
   409         attrs.setdefault('onkeyup', 'autogrow(this)')
       
   410         if not values:
       
   411             value = u''
       
   412         elif len(values) == 1:
       
   413             value = values[0]
       
   414         else:
       
   415             raise ValueError('a textarea is not supposed to be multivalued')
       
   416         lines = value.splitlines()
       
   417         linecount = len(lines)
       
   418         for line in lines:
       
   419             linecount += len(line) // self._columns
       
   420         attrs.setdefault('cols', self._columns)
       
   421         attrs.setdefault('rows', min(self._maxrows, linecount + self._minrows))
       
   422         return tags.textarea(value, name=field.input_name(form, self.suffix),
       
   423                              **attrs)
       
   424 
       
   425 
       
   426 class FCKEditor(TextArea):
       
   427     """FCKEditor enabled <textarea>, will return a unicode string containing
       
   428     HTML formated text.
       
   429     """
       
   430     def __init__(self, *args, **kwargs):
       
   431         super(FCKEditor, self).__init__(*args, **kwargs)
       
   432         self.attrs['cubicweb:type'] = 'wysiwyg'
       
   433 
       
   434     def _render(self, form, field, renderer):
       
   435         form._cw.fckeditor_config()
       
   436         return super(FCKEditor, self)._render(form, field, renderer)
       
   437 
       
   438 
       
   439 class Select(FieldWidget):
       
   440     """Simple <select>, for field having a specific vocabulary. Will return
       
   441     a unicode string, or a list of unicode strings.
       
   442     """
       
   443     vocabulary_widget = True
       
   444     default_size = 10
       
   445 
       
   446     def __init__(self, attrs=None, multiple=False, **kwargs):
       
   447         super(Select, self).__init__(attrs, **kwargs)
       
   448         self._multiple = multiple
       
   449 
       
   450     def _render(self, form, field, renderer):
       
   451         curvalues, attrs = self.values_and_attributes(form, field)
       
   452         options = []
       
   453         optgroup_opened = False
       
   454         vocab = field.vocabulary(form)
       
   455         for option in vocab:
       
   456             try:
       
   457                 label, value, oattrs = option
       
   458             except ValueError:
       
   459                 label, value = option
       
   460                 oattrs = {}
       
   461             if value is None:
       
   462                 # handle separator
       
   463                 if optgroup_opened:
       
   464                     options.append(u'</optgroup>')
       
   465                 oattrs.setdefault('label', label or '')
       
   466                 options.append(u'<optgroup %s>' % uilib.sgml_attributes(oattrs))
       
   467                 optgroup_opened = True
       
   468             elif self.value_selected(value, curvalues):
       
   469                 options.append(tags.option(label, value=value,
       
   470                                            selected='selected', **oattrs))
       
   471             else:
       
   472                 options.append(tags.option(label, value=value, **oattrs))
       
   473         if optgroup_opened:
       
   474             options.append(u'</optgroup>')
       
   475         if 'size' not in attrs:
       
   476             if self._multiple:
       
   477                 size = text_type(min(self.default_size, len(vocab) or 1))
       
   478             else:
       
   479                 size = u'1'
       
   480             attrs['size'] = size
       
   481         return tags.select(name=field.input_name(form, self.suffix),
       
   482                            multiple=self._multiple, options=options, **attrs)
       
   483 
       
   484     def value_selected(self, value, curvalues):
       
   485         return value in curvalues
       
   486 
       
   487 
       
   488 class InOutWidget(Select):
       
   489     needs_js = ('cubicweb.widgets.js', )
       
   490     default_size = 10
       
   491     template = """
       
   492 <table id="%(widgetid)s">
       
   493   <tr>
       
   494     <td>%(inoutinput)s</td>
       
   495     <td><div style="margin-bottom:3px">%(addinput)s</div>
       
   496         <div>%(removeinput)s</div>
       
   497     </td>
       
   498     <td>%(resinput)s</td>
       
   499   </tr>
       
   500 </table>
       
   501 """
       
   502     add_button = ('<input type="button" class="wdgButton cwinoutadd" '
       
   503                   'value="&gt;&gt;" size="10" />')
       
   504     remove_button = ('<input type="button" class="wdgButton cwinoutremove" '
       
   505                      'value="&lt;&lt;" size="10" />')
       
   506 
       
   507     def __init__(self, *args, **kwargs):
       
   508         super(InOutWidget, self).__init__(*args, **kwargs)
       
   509         self._multiple = True
       
   510 
       
   511     def render_select(self, form, field, name, selected=False):
       
   512         values, attrs = self.values_and_attributes(form, field)
       
   513         options = []
       
   514         inputs = []
       
   515         for option in field.vocabulary(form):
       
   516             try:
       
   517                 label, value, _oattrs = option
       
   518             except ValueError:
       
   519                 label, value = option
       
   520             if selected:
       
   521                 # add values
       
   522                 if value in values:
       
   523                     options.append(tags.option(label, value=value))
       
   524                     # add hidden inputs
       
   525                     inputs.append(tags.input(value=value,
       
   526                                              name=field.dom_id(form),
       
   527                                              type="hidden"))
       
   528             else:
       
   529                 if value not in values:
       
   530                     options.append(tags.option(label, value=value))
       
   531         if 'size' not in attrs:
       
   532             attrs['size'] = self.default_size
       
   533         if 'id' in attrs:
       
   534             attrs.pop('id')
       
   535         return tags.select(name=name, multiple=self._multiple, id=name,
       
   536                            options=options, **attrs) + '\n'.join(inputs)
       
   537 
       
   538     def _render(self, form, field, renderer):
       
   539         domid = field.dom_id(form)
       
   540         jsnodes = {'widgetid': domid,
       
   541                    'from': 'from_' + domid,
       
   542                    'to': 'to_' + domid}
       
   543         form._cw.add_onload(u'$(cw.jqNode("%s")).cwinoutwidget("%s", "%s");'
       
   544                             % (jsnodes['widgetid'], jsnodes['from'], jsnodes['to']))
       
   545         field.required = True
       
   546         return (self.template %
       
   547                 {'widgetid': jsnodes['widgetid'],
       
   548                  # helpinfo select tag
       
   549                  'inoutinput': self.render_select(form, field, jsnodes['from']),
       
   550                  # select tag with resultats
       
   551                  'resinput': self.render_select(form, field, jsnodes['to'], selected=True),
       
   552                  'addinput': self.add_button % jsnodes,
       
   553                  'removeinput': self.remove_button % jsnodes
       
   554                  })
       
   555 
       
   556 
       
   557 class BitSelect(Select):
       
   558     """Select widget for IntField using a vocabulary with bit masks as values.
       
   559 
       
   560     See also :class:`~cubicweb.web.facet.BitFieldFacet`.
       
   561     """
       
   562     def __init__(self, attrs=None, multiple=True, **kwargs):
       
   563         super(BitSelect, self).__init__(attrs, multiple=multiple, **kwargs)
       
   564 
       
   565     def value_selected(self, value, curvalues):
       
   566         mask = reduce(lambda x, y: int(x) | int(y), curvalues, 0)
       
   567         return int(value) & mask
       
   568 
       
   569     def process_field_data(self, form, field):
       
   570         """Return process posted value(s) for widget and return something
       
   571         understandable by the associated `field`. That value may be correctly
       
   572         typed or a string that the field may parse.
       
   573         """
       
   574         val = super(BitSelect, self).process_field_data(form, field)
       
   575         if isinstance(val, list):
       
   576             val = reduce(lambda x, y: int(x) | int(y), val, 0)
       
   577         elif val:
       
   578             val = int(val)
       
   579         else:
       
   580             val = 0
       
   581         return val
       
   582 
       
   583 
       
   584 class CheckBox(Input):
       
   585     """Simple <input type='checkbox'>, for field having a specific
       
   586     vocabulary. One input will be generated for each possible value.
       
   587 
       
   588     You can specify separator using the `separator` constructor argument, by
       
   589     default <br/> is used.
       
   590     """
       
   591     type = 'checkbox'
       
   592     default_separator = u'<br/>\n'
       
   593     vocabulary_widget = True
       
   594 
       
   595     def __init__(self, attrs=None, separator=None, **kwargs):
       
   596         super(CheckBox, self).__init__(attrs, **kwargs)
       
   597         self.separator = separator or self.default_separator
       
   598 
       
   599     def _render(self, form, field, renderer):
       
   600         curvalues, attrs = self.values_and_attributes(form, field)
       
   601         domid = attrs.pop('id', None)
       
   602         sep = self.separator
       
   603         options = []
       
   604         for i, option in enumerate(field.vocabulary(form)):
       
   605             try:
       
   606                 label, value, oattrs = option
       
   607             except ValueError:
       
   608                 label, value = option
       
   609                 oattrs = {}
       
   610             iattrs = attrs.copy()
       
   611             iattrs.update(oattrs)
       
   612             if i == 0 and domid is not None:
       
   613                 iattrs.setdefault('id', domid)
       
   614             if value in curvalues:
       
   615                 iattrs['checked'] = u'checked'
       
   616             tag = tags.input(name=field.input_name(form, self.suffix),
       
   617                              type=self.type, value=value, **iattrs)
       
   618             options.append(u'<label>%s&#160;%s</label>' % (tag, xml_escape(label)))
       
   619         return sep.join(options)
       
   620 
       
   621 
       
   622 class Radio(CheckBox):
       
   623     """Simle <input type='radio'>, for field having a specific vocabulary. One
       
   624     input will be generated for each possible value.
       
   625 
       
   626     You can specify separator using the `separator` constructor argument, by
       
   627     default <br/> is used.
       
   628     """
       
   629     type = 'radio'
       
   630 
       
   631 
       
   632 # javascript widgets ###########################################################
       
   633 
       
   634 class DateTimePicker(TextInput):
       
   635     """<input type='text'> + javascript date/time picker for date or datetime
       
   636     fields. Will return the date or datetime as a unicode string.
       
   637     """
       
   638     monthnames = ('january', 'february', 'march', 'april',
       
   639                   'may', 'june', 'july', 'august',
       
   640                   'september', 'october', 'november', 'december')
       
   641     daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
       
   642                 'friday', 'saturday', 'sunday')
       
   643 
       
   644     needs_js = ('cubicweb.calendar.js',)
       
   645     needs_css = ('cubicweb.calendar_popup.css',)
       
   646 
       
   647     @classmethod
       
   648     def add_localized_infos(cls, req):
       
   649         """inserts JS variables defining localized months and days"""
       
   650         _ = req._
       
   651         monthnames = [_(mname) for mname in cls.monthnames]
       
   652         daynames = [_(dname) for dname in cls.daynames]
       
   653         req.html_headers.define_var('MONTHNAMES', monthnames)
       
   654         req.html_headers.define_var('DAYNAMES', daynames)
       
   655 
       
   656     def _render(self, form, field, renderer):
       
   657         txtwidget = super(DateTimePicker, self)._render(form, field, renderer)
       
   658         self.add_localized_infos(form._cw)
       
   659         cal_button = self._render_calendar_popup(form, field)
       
   660         return txtwidget + cal_button
       
   661 
       
   662     def _render_calendar_popup(self, form, field):
       
   663         value = field.typed_value(form)
       
   664         if not value:
       
   665             value = date.today()
       
   666         inputid = field.dom_id(form)
       
   667         helperid = '%shelper' % inputid
       
   668         year, month = value.year, value.month
       
   669         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
       
   670 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
       
   671                 % (helperid, inputid, year, month,
       
   672                    form._cw.uiprops['CALENDAR_ICON'],
       
   673                    form._cw._('calendar'), helperid))
       
   674 
       
   675 
       
   676 class JQueryDatePicker(FieldWidget):
       
   677     """Use jquery.ui.datepicker to define a date picker. Will return the date as
       
   678     a unicode string.
       
   679 
       
   680     You can couple DatePickers by using the min_of and/or max_of parameters.
       
   681     The DatePicker identified by the value of min_of(/max_of) will force the user to
       
   682     choose a date anterior(/posterior) to this DatePicker.
       
   683 
       
   684     example:
       
   685     start and end are two JQueryDatePicker and start must always be before end
       
   686         affk.set_field_kwargs(etype, 'start_date', widget=JQueryDatePicker(min_of='end_date'))
       
   687         affk.set_field_kwargs(etype, 'end_date', widget=JQueryDatePicker(max_of='start_date'))
       
   688     That way, on change of end(/start) value a new max(/min) will be set for start(/end)
       
   689     The invalid dates will be gray colored in the datepicker
       
   690     """
       
   691     needs_js = ('jquery.ui.js', )
       
   692     needs_css = ('jquery.ui.css',)
       
   693     default_size = 10
       
   694 
       
   695     def __init__(self, datestr=None, min_of=None, max_of=None, **kwargs):
       
   696         super(JQueryDatePicker, self).__init__(**kwargs)
       
   697         self.min_of = min_of
       
   698         self.max_of = max_of
       
   699         self.value = datestr
       
   700 
       
   701     def attributes(self, form, field):
       
   702         form._cw.add_js('cubicweb.widgets.js')
       
   703         attrs = super(JQueryDatePicker, self).attributes(form, field)
       
   704         if self.max_of:
       
   705             attrs['data-max-of'] = '%s-subject:%s' % (self.max_of, form.edited_entity.eid)
       
   706         if self.min_of:
       
   707             attrs['data-min-of'] = '%s-subject:%s' % (self.min_of, form.edited_entity.eid)
       
   708         return attrs
       
   709 
       
   710     def _render(self, form, field, renderer):
       
   711         req = form._cw
       
   712         if req.lang != 'en':
       
   713             req.add_js('jquery.ui.datepicker-%s.js' % req.lang)
       
   714         domid = field.dom_id(form, self.suffix)
       
   715         # XXX find a way to understand every format
       
   716         fmt = req.property_value('ui.date-format')
       
   717         picker_fmt = fmt.replace('%Y', 'yy').replace('%m', 'mm').replace('%d', 'dd')
       
   718         max_date = min_date = None
       
   719         if self.min_of:
       
   720             current = getattr(form.edited_entity, self.min_of)
       
   721             if current is not None:
       
   722                 max_date = current.strftime(fmt)
       
   723         if self.max_of:
       
   724             current = getattr(form.edited_entity, self.max_of)
       
   725             if current is not None:
       
   726                 min_date = current.strftime(fmt)
       
   727         req.add_onload(u'renderJQueryDatePicker("%s", "%s", "%s", %s, %s);'
       
   728                        % (domid, req.uiprops['CALENDAR_ICON'], picker_fmt, json_dumps(min_date),
       
   729                           json_dumps(max_date)))
       
   730         return self._render_input(form, field)
       
   731 
       
   732     def _render_input(self, form, field):
       
   733         if self.value is None:
       
   734             value = self.values(form, field)[0]
       
   735         else:
       
   736             value = self.value
       
   737         attrs = self.attributes(form, field)
       
   738         attrs.setdefault('size', text_type(self.default_size))
       
   739         return tags.input(name=field.input_name(form, self.suffix),
       
   740                           value=value, type='text', **attrs)
       
   741 
       
   742 
       
   743 class JQueryTimePicker(JQueryDatePicker):
       
   744     """Use jquery.timePicker to define a time picker. Will return the time as a
       
   745     unicode string.
       
   746     """
       
   747     needs_js = ('jquery.timePicker.js',)
       
   748     needs_css = ('jquery.timepicker.css',)
       
   749     default_size = 5
       
   750 
       
   751     def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
       
   752         super(JQueryTimePicker, self).__init__(timestr, **kwargs)
       
   753         self.timesteps = timesteps
       
   754         self.separator = separator
       
   755 
       
   756     def _render(self, form, field, renderer):
       
   757         domid = field.dom_id(form, self.suffix)
       
   758         form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
       
   759             domid, self.timesteps, self.separator))
       
   760         return self._render_input(form, field)
       
   761 
       
   762 
       
   763 class JQueryDateTimePicker(FieldWidget):
       
   764     """Compound widget using :class:`JQueryDatePicker` and
       
   765     :class:`JQueryTimePicker` widgets to define a date and time picker. Will
       
   766     return the date and time as python datetime instance.
       
   767     """
       
   768     def __init__(self, initialtime=None, timesteps=15, **kwargs):
       
   769         super(JQueryDateTimePicker, self).__init__(**kwargs)
       
   770         self.initialtime = initialtime
       
   771         self.timesteps = timesteps
       
   772 
       
   773     def _render(self, form, field, renderer):
       
   774         """render the widget for the given `field` of `form`.
       
   775 
       
   776         Generate one <input> tag for each field's value
       
   777         """
       
   778         req = form._cw
       
   779         dateqname = field.input_name(form, 'date')
       
   780         timeqname = field.input_name(form, 'time')
       
   781         if dateqname in form.form_previous_values:
       
   782             datestr = form.form_previous_values[dateqname]
       
   783             timestr = form.form_previous_values[timeqname]
       
   784         else:
       
   785             datestr = timestr = u''
       
   786             if field.name in req.form:
       
   787                 value = req.parse_datetime(req.form[field.name])
       
   788             else:
       
   789                 value = self.typed_value(form, field)
       
   790             if value:
       
   791                 datestr = req.format_date(value)
       
   792                 timestr = req.format_time(value)
       
   793             elif self.initialtime:
       
   794                 timestr = req.format_time(self.initialtime)
       
   795         datepicker = JQueryDatePicker(datestr=datestr, suffix='date')
       
   796         timepicker = JQueryTimePicker(timestr=timestr, timesteps=self.timesteps,
       
   797                                       suffix='time')
       
   798         return u'<div id="%s">%s%s</div>' % (field.dom_id(form),
       
   799                                              datepicker.render(form, field, renderer),
       
   800                                              timepicker.render(form, field, renderer))
       
   801 
       
   802     def process_field_data(self, form, field):
       
   803         req = form._cw
       
   804         datestr = req.form.get(field.input_name(form, 'date')).strip() or None
       
   805         timestr = req.form.get(field.input_name(form, 'time')).strip() or None
       
   806         if datestr is None:
       
   807             return None
       
   808         try:
       
   809             date = todatetime(req.parse_datetime(datestr, 'Date'))
       
   810         except ValueError as exc:
       
   811             raise ProcessFormError(text_type(exc))
       
   812         if timestr is None:
       
   813             return date
       
   814         try:
       
   815             time = req.parse_datetime(timestr, 'Time')
       
   816         except ValueError as exc:
       
   817             raise ProcessFormError(text_type(exc))
       
   818         return date.replace(hour=time.hour, minute=time.minute, second=time.second)
       
   819 
       
   820 
       
   821 # ajax widgets ################################################################
       
   822 
       
   823 def init_ajax_attributes(attrs, wdgtype, loadtype=u'auto'):
       
   824     try:
       
   825         attrs['class'] += u' widget'
       
   826     except KeyError:
       
   827         attrs['class'] = u'widget'
       
   828     attrs.setdefault('cubicweb:wdgtype', wdgtype)
       
   829     attrs.setdefault('cubicweb:loadtype', loadtype)
       
   830 
       
   831 
       
   832 class AjaxWidget(FieldWidget):
       
   833     """Simple <div> based ajax widget, requiring a `wdgtype` argument telling
       
   834     which javascript widget should be used.
       
   835     """
       
   836     def __init__(self, wdgtype, inputid=None, **kwargs):
       
   837         super(AjaxWidget, self).__init__(**kwargs)
       
   838         init_ajax_attributes(self.attrs, wdgtype)
       
   839         if inputid is not None:
       
   840             self.attrs['cubicweb:inputid'] = inputid
       
   841 
       
   842     def _render(self, form, field, renderer):
       
   843         attrs = self.values_and_attributes(form, field)[-1]
       
   844         return tags.div(**attrs)
       
   845 
       
   846 
       
   847 class AutoCompletionWidget(TextInput):
       
   848     """<input type='text'> based ajax widget, taking a `autocomplete_initfunc`
       
   849     argument which should specify the name of a method of the json
       
   850     controller. This method is expected to return allowed values for the input,
       
   851     that the widget will use to propose matching values as you type.
       
   852     """
       
   853     needs_js = ('cubicweb.widgets.js', 'jquery.ui.js')
       
   854     needs_css = ('jquery.ui.css',)
       
   855     default_settings = {}
       
   856 
       
   857     def __init__(self, *args, **kwargs):
       
   858         self.autocomplete_settings = kwargs.pop('autocomplete_settings',
       
   859                                                 self.default_settings)
       
   860         self.autocomplete_initfunc = kwargs.pop('autocomplete_initfunc')
       
   861         super(AutoCompletionWidget, self).__init__(*args, **kwargs)
       
   862 
       
   863     def values(self, form, field):
       
   864         values = super(AutoCompletionWidget, self).values(form, field)
       
   865         if not values:
       
   866             values = ('',)
       
   867         return values
       
   868 
       
   869     def _render(self, form, field, renderer):
       
   870         entity = form.edited_entity
       
   871         domid = field.dom_id(form).replace(':', r'\\:')
       
   872         if callable(self.autocomplete_initfunc):
       
   873             data = self.autocomplete_initfunc(form, field)
       
   874         else:
       
   875             data = xml_escape(self._get_url(entity, field))
       
   876         form._cw.add_onload(u'$("#%s").cwautocomplete(%s, %s);'
       
   877                             % (domid, json_dumps(data),
       
   878                                json_dumps(self.autocomplete_settings)))
       
   879         return super(AutoCompletionWidget, self)._render(form, field, renderer)
       
   880 
       
   881     def _get_url(self, entity, field):
       
   882         fname = self.autocomplete_initfunc
       
   883         return entity._cw.build_url('ajax', fname=fname, mode='remote',
       
   884                                     pageid=entity._cw.pageid)
       
   885 
       
   886 
       
   887 class StaticFileAutoCompletionWidget(AutoCompletionWidget):
       
   888     """XXX describe me"""
       
   889     wdgtype = 'StaticFileSuggestField'
       
   890 
       
   891     def _get_url(self, entity, field):
       
   892         return entity._cw.data_url(self.autocomplete_initfunc)
       
   893 
       
   894 
       
   895 class RestrictedAutoCompletionWidget(AutoCompletionWidget):
       
   896     """XXX describe me"""
       
   897     default_settings = {'mustMatch': True}
       
   898 
       
   899 
       
   900 class LazyRestrictedAutoCompletionWidget(RestrictedAutoCompletionWidget):
       
   901     """remote autocomplete """
       
   902 
       
   903     def values_and_attributes(self, form, field):
       
   904         """override values_and_attributes to handle initial displayed values"""
       
   905         values, attrs = super(LazyRestrictedAutoCompletionWidget, self).values_and_attributes(
       
   906             form, field)
       
   907         assert len(values) == 1, "multiple selection is not supported yet by LazyWidget"
       
   908         if not values[0]:
       
   909             values = form.cw_extra_kwargs.get(field.name, '')
       
   910             if not isinstance(values, (tuple, list)):
       
   911                 values = (values,)
       
   912         try:
       
   913             values = list(values)
       
   914             values[0] = int(values[0])
       
   915             attrs['cubicweb:initialvalue'] = values[0]
       
   916             values = (self.display_value_for(form, values[0]),)
       
   917         except (TypeError, ValueError):
       
   918             pass
       
   919         return values, attrs
       
   920 
       
   921     def display_value_for(self, form, value):
       
   922         entity = form._cw.entity_from_eid(value)
       
   923         return entity.view('combobox')
       
   924 
       
   925 
       
   926 # more widgets #################################################################
       
   927 
       
   928 class IntervalWidget(FieldWidget):
       
   929     """Custom widget to display an interval composed by 2 fields. This widget is
       
   930     expected to be used with a :class:`CompoundField` containing the two actual
       
   931     fields.
       
   932 
       
   933     Exemple usage::
       
   934 
       
   935       class MyForm(FieldsForm):
       
   936          price = CompoundField(fields=(IntField(name='minprice'),
       
   937                                        IntField(name='maxprice')),
       
   938                                label=_('price'),
       
   939                                widget=IntervalWidget())
       
   940     """
       
   941     def _render(self, form, field, renderer):
       
   942         actual_fields = field.fields
       
   943         assert len(actual_fields) == 2
       
   944         return u'<div>%s %s %s %s</div>' % (
       
   945             form._cw._('from_interval_start'),
       
   946             actual_fields[0].render(form, renderer),
       
   947             form._cw._('to_interval_end'),
       
   948             actual_fields[1].render(form, renderer),
       
   949         )
       
   950 
       
   951 
       
   952 class HorizontalLayoutWidget(FieldWidget):
       
   953     """Custom widget to display a set of fields grouped together horizontally in
       
   954     a form. See `IntervalWidget` for example usage.
       
   955     """
       
   956     def _render(self, form, field, renderer):
       
   957         if self.attrs.get('display_label', True):
       
   958             subst = self.attrs.get('label_input_substitution', '%(label)s %(input)s')
       
   959             fields = [subst % {'label': renderer.render_label(form, f),
       
   960                                'input': f.render(form, renderer)}
       
   961                       for f in field.subfields(form)]
       
   962         else:
       
   963             fields = [f.render(form, renderer) for f in field.subfields(form)]
       
   964         return u'<div>%s</div>' % ' '.join(fields)
       
   965 
       
   966 
       
   967 class EditableURLWidget(FieldWidget):
       
   968     """Custom widget to edit separatly a URL path / query string (used by
       
   969     default for the `path` attribute of `Bookmark` entities).
       
   970 
       
   971     It deals with url quoting nicely so that the user edit the unquoted value.
       
   972     """
       
   973 
       
   974     def _render(self, form, field, renderer):
       
   975         assert self.suffix is None, 'not supported'
       
   976         req = form._cw
       
   977         pathqname = field.input_name(form, 'path')
       
   978         fqsqname = field.input_name(form, 'fqs')  # formatted query string
       
   979         if pathqname in form.form_previous_values:
       
   980             path = form.form_previous_values[pathqname]
       
   981             fqs = form.form_previous_values[fqsqname]
       
   982         else:
       
   983             if field.name in req.form:
       
   984                 value = req.form[field.name]
       
   985             else:
       
   986                 value = self.typed_value(form, field)
       
   987             if value:
       
   988                 try:
       
   989                     path, qs = value.split('?', 1)
       
   990                 except ValueError:
       
   991                     path = value
       
   992                     qs = ''
       
   993             else:
       
   994                 path = qs = ''
       
   995             fqs = u'\n'.join(u'%s=%s' % (k, v) for k, v in req.url_parse_qsl(qs))
       
   996         attrs = dict(self.attrs)
       
   997         if self.setdomid:
       
   998             attrs['id'] = field.dom_id(form)
       
   999         if self.settabindex and 'tabindex' not in attrs:
       
  1000             attrs['tabindex'] = req.next_tabindex()
       
  1001         # ensure something is rendered
       
  1002         inputs = [u'<table><tr><th>',
       
  1003                   req._('i18n_bookmark_url_path'),
       
  1004                   u'</th><td>',
       
  1005                   tags.input(name=pathqname, type='string', value=path, **attrs),
       
  1006                   u'</td></tr><tr><th>',
       
  1007                   req._('i18n_bookmark_url_fqs'),
       
  1008                   u'</th><td>']
       
  1009         if self.setdomid:
       
  1010             attrs['id'] = field.dom_id(form, 'fqs')
       
  1011         if self.settabindex:
       
  1012             attrs['tabindex'] = req.next_tabindex()
       
  1013         attrs.setdefault('cols', 60)
       
  1014         attrs.setdefault('onkeyup', 'autogrow(this)')
       
  1015         inputs += [tags.textarea(fqs, name=fqsqname, **attrs),
       
  1016                    u'</td></tr></table>']
       
  1017         # surrounding div necessary for proper error localization
       
  1018         return u'<div id="%s">%s</div>' % (
       
  1019             field.dom_id(form), u'\n'.join(inputs))
       
  1020 
       
  1021     def process_field_data(self, form, field):
       
  1022         req = form._cw
       
  1023         values = {}
       
  1024         path = req.form.get(field.input_name(form, 'path'))
       
  1025         if isinstance(path, string_types):
       
  1026             path = path.strip()
       
  1027         if path is None:
       
  1028             path = u''
       
  1029         fqs = req.form.get(field.input_name(form, 'fqs'))
       
  1030         if isinstance(fqs, string_types):
       
  1031             fqs = fqs.strip() or None
       
  1032             if fqs:
       
  1033                 for i, line in enumerate(fqs.split('\n')):
       
  1034                     line = line.strip()
       
  1035                     if line:
       
  1036                         try:
       
  1037                             key, val = line.split('=', 1)
       
  1038                         except ValueError:
       
  1039                             msg = req._("wrong query parameter line %s") % (i + 1)
       
  1040                             raise ProcessFormError(msg)
       
  1041                         # value will be url quoted by build_url_params
       
  1042                         values.setdefault(key, []).append(val)
       
  1043         if not values:
       
  1044             return path
       
  1045         return u'%s?%s' % (path, req.build_url_params(**values))
       
  1046 
       
  1047 
       
  1048 # form controls ######################################################################
       
  1049 
       
  1050 class Button(Input):
       
  1051     """Simple <input type='button'>, base class for global form buttons.
       
  1052 
       
  1053     Note that `label` is a msgid which will be translated at form generation
       
  1054     time, you should not give an already translated string.
       
  1055     """
       
  1056     type = 'button'
       
  1057     css_class = 'validateButton'
       
  1058 
       
  1059     def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
       
  1060                  setdomid=None, settabindex=None,
       
  1061                  name='', value='', onclick=None, cwaction=None):
       
  1062         super(Button, self).__init__(attrs, setdomid, settabindex)
       
  1063         if isinstance(label, tuple):
       
  1064             self.label = label[0]
       
  1065             self.icon = label[1]
       
  1066         else:
       
  1067             self.label = label
       
  1068             self.icon = None
       
  1069         self.name = name
       
  1070         self.value = ''
       
  1071         self.onclick = onclick
       
  1072         self.cwaction = cwaction
       
  1073 
       
  1074     def render(self, form, field=None, renderer=None):
       
  1075         label = form._cw._(self.label)
       
  1076         attrs = self.attrs.copy()
       
  1077         attrs.setdefault('class', self.css_class)
       
  1078         if self.cwaction:
       
  1079             assert self.onclick is None
       
  1080             attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
       
  1081                 self.cwaction, self.label, form.domid)
       
  1082         elif self.onclick:
       
  1083             attrs['onclick'] = self.onclick
       
  1084         if self.name:
       
  1085             attrs['name'] = self.name
       
  1086             if self.setdomid:
       
  1087                 attrs['id'] = self.name
       
  1088         if self.settabindex and 'tabindex' not in attrs:
       
  1089             attrs['tabindex'] = form._cw.next_tabindex()
       
  1090         if self.icon:
       
  1091             img = tags.img(src=form._cw.uiprops[self.icon], alt=self.icon)
       
  1092         else:
       
  1093             img = u''
       
  1094         return tags.button(img + xml_escape(label), escapecontent=False,
       
  1095                            value=label, type=self.type, **attrs)
       
  1096 
       
  1097 
       
  1098 class SubmitButton(Button):
       
  1099     """Simple <input type='submit'>, main button to submit a form"""
       
  1100     type = 'submit'
       
  1101 
       
  1102 
       
  1103 class ResetButton(Button):
       
  1104     """Simple <input type='reset'>, main button to reset a form. You usually
       
  1105     don't want to use this.
       
  1106     """
       
  1107     type = 'reset'
       
  1108 
       
  1109 
       
  1110 class ImgButton(object):
       
  1111     """Simple <img> wrapped into a <a> tag with href triggering something (usually a
       
  1112     javascript call).
       
  1113     """
       
  1114     def __init__(self, domid, href, label, imgressource):
       
  1115         self.domid = domid
       
  1116         self.href = href
       
  1117         self.imgressource = imgressource
       
  1118         self.label = label
       
  1119 
       
  1120     def render(self, form, field=None, renderer=None):
       
  1121         label = form._cw._(self.label)
       
  1122         imgsrc = form._cw.uiprops[self.imgressource]
       
  1123         return '<a id="%(domid)s" href="%(href)s">'\
       
  1124                '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
       
  1125                    'label': label, 'imgsrc': imgsrc,
       
  1126                    'domid': self.domid, 'href': self.href}