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