web/views/formrenderers.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2014 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 Renderers
       
    20 ---------
       
    21 
       
    22 .. Note::
       
    23    Form renderers are responsible to layout a form to HTML.
       
    24 
       
    25 Here are the base renderers available:
       
    26 
       
    27 .. autoclass:: cubicweb.web.views.formrenderers.FormRenderer
       
    28 .. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer
       
    29 .. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer
       
    30 .. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer
       
    31 .. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer
       
    32 
       
    33 """
       
    34 
       
    35 __docformat__ = "restructuredtext en"
       
    36 from cubicweb import _
       
    37 
       
    38 from warnings import warn
       
    39 
       
    40 from six import text_type
       
    41 
       
    42 from logilab.mtconverter import xml_escape
       
    43 from logilab.common.registry import yes
       
    44 
       
    45 from cubicweb import tags, uilib
       
    46 from cubicweb.appobject import AppObject
       
    47 from cubicweb.predicates import is_instance
       
    48 from cubicweb.utils import json_dumps, support_args
       
    49 from cubicweb.web import eid_param, formwidgets as fwdgs
       
    50 
       
    51 
       
    52 def checkbox(name, value, attrs='', checked=None):
       
    53     if checked is None:
       
    54         checked = value
       
    55     checked = checked and 'checked="checked"' or ''
       
    56     return u'<input type="checkbox" name="%s" value="%s" %s %s />' % (
       
    57         name, value, checked, attrs)
       
    58 
       
    59 def field_label(form, field):
       
    60     if callable(field.label):
       
    61         return field.label(form, field)
       
    62     # XXX with 3.6 we can now properly rely on 'if field.role is not None' and
       
    63     # stop having a tuple for label
       
    64     if isinstance(field.label, tuple): # i.e. needs contextual translation
       
    65         return form._cw.pgettext(*field.label)
       
    66     return form._cw._(field.label)
       
    67 
       
    68 
       
    69 
       
    70 class FormRenderer(AppObject):
       
    71     """This is the 'default' renderer, displaying fields in a two columns table:
       
    72 
       
    73     +--------------+--------------+
       
    74     | field1 label | field1 input |
       
    75     +--------------+--------------+
       
    76     | field2 label | field2 input |
       
    77     +--------------+--------------+
       
    78 
       
    79     +---------+
       
    80     | buttons |
       
    81     +---------+
       
    82     """
       
    83     __registry__ = 'formrenderers'
       
    84     __regid__ = 'default'
       
    85 
       
    86     _options = ('display_label', 'display_help',
       
    87                 'display_progress_div', 'table_class', 'button_bar_class',
       
    88                 # add entity since it may be given to select the renderer
       
    89                 'entity')
       
    90     display_label = True
       
    91     display_help = True
       
    92     display_progress_div = True
       
    93     table_class = u'attributeForm'
       
    94     button_bar_class = u'formButtonBar'
       
    95 
       
    96     def __init__(self, req=None, rset=None, row=None, col=None, **kwargs):
       
    97         super(FormRenderer, self).__init__(req, rset=rset, row=row, col=col)
       
    98         if self._set_options(kwargs):
       
    99             raise ValueError('unconsumed arguments %s' % kwargs)
       
   100 
       
   101     def _set_options(self, kwargs):
       
   102         for key in self._options:
       
   103             try:
       
   104                 setattr(self, key, kwargs.pop(key))
       
   105             except KeyError:
       
   106                 continue
       
   107         return kwargs
       
   108 
       
   109     # renderer interface ######################################################
       
   110 
       
   111     def render(self, w, form, values):
       
   112         self._set_options(values)
       
   113         form.add_media()
       
   114         data = []
       
   115         _w = data.append
       
   116         _w(self.open_form(form, values))
       
   117         self.render_content(_w, form, values)
       
   118         _w(self.close_form(form, values))
       
   119         errormsg = self.error_message(form)
       
   120         if errormsg:
       
   121             data.insert(0, errormsg)
       
   122         # NOTE: we call unicode because `tag` objects may be found within data
       
   123         #       e.g. from the cwtags library
       
   124         w(''.join(text_type(x) for x in data))
       
   125 
       
   126     def render_content(self, w, form, values):
       
   127         if self.display_progress_div:
       
   128             w(u'<div id="progress">%s</div>' % self._cw._('validating...'))
       
   129         w(u'\n<fieldset>\n')
       
   130         self.render_fields(w, form, values)
       
   131         self.render_buttons(w, form)
       
   132         w(u'\n</fieldset>\n')
       
   133 
       
   134     def render_label(self, form, field):
       
   135         if field.label is None:
       
   136             return u''
       
   137         label = field_label(form, field)
       
   138         attrs = {'for': field.dom_id(form)}
       
   139         if field.required:
       
   140             attrs['class'] = 'required'
       
   141         return tags.label(label, **attrs)
       
   142 
       
   143     def render_help(self, form, field):
       
   144         help = []
       
   145         descr = field.help
       
   146         if callable(descr):
       
   147             descr = descr(form, field)
       
   148         if descr:
       
   149             help.append('<div class="helper">%s</div>' % self._cw._(descr))
       
   150         example = field.example_format(self._cw)
       
   151         if example:
       
   152             help.append('<div class="helper">(%s: %s)</div>'
       
   153                         % (self._cw._('sample format'), example))
       
   154         return u'&#160;'.join(help)
       
   155 
       
   156     # specific methods (mostly to ease overriding) #############################
       
   157 
       
   158     def error_message(self, form):
       
   159         """return formatted error message
       
   160 
       
   161         This method should be called once inlined field errors has been consumed
       
   162         """
       
   163         req = self._cw
       
   164         errex = form.form_valerror
       
   165         # get extra errors
       
   166         if errex is not None:
       
   167             errormsg = req._('please correct the following errors:')
       
   168             errors = form.remaining_errors()
       
   169             if errors:
       
   170                 if len(errors) > 1:
       
   171                     templstr = u'<li>%s</li>\n'
       
   172                 else:
       
   173                     templstr = u'&#160;%s\n'
       
   174                 for field, err in errors:
       
   175                     if field is None:
       
   176                         errormsg += templstr % err
       
   177                     else:
       
   178                         errormsg += templstr % '%s: %s' % (req._(field), err)
       
   179                 if len(errors) > 1:
       
   180                     errormsg = '<ul>%s</ul>' % errormsg
       
   181             return u'<div class="errorMessage">%s</div>' % errormsg
       
   182         return u''
       
   183 
       
   184     def open_form(self, form, values, **attrs):
       
   185         if form.needs_multipart:
       
   186             enctype = u'multipart/form-data'
       
   187         else:
       
   188             enctype = u'application/x-www-form-urlencoded'
       
   189         attrs.setdefault('enctype', enctype)
       
   190         attrs.setdefault('method', 'post')
       
   191         attrs.setdefault('action', form.form_action() or '#')
       
   192         if form.domid:
       
   193             attrs.setdefault('id', form.domid)
       
   194         if form.onsubmit:
       
   195             attrs.setdefault('onsubmit',  form.onsubmit)
       
   196         if form.cssstyle:
       
   197             attrs.setdefault('style', form.cssstyle)
       
   198         if form.cssclass:
       
   199             attrs.setdefault('class', form.cssclass)
       
   200         if form.cwtarget:
       
   201             attrs.setdefault('target', form.cwtarget)
       
   202         if not form.autocomplete:
       
   203             attrs.setdefault('autocomplete', 'off')
       
   204         return '<form %s>' % uilib.sgml_attributes(attrs)
       
   205 
       
   206     def close_form(self, form, values):
       
   207         """seems dumb but important for consistency w/ close form, and necessary
       
   208         for form renderers overriding open_form to use something else or more than
       
   209         and <form>
       
   210         """
       
   211         out = u'</form>'
       
   212         if form.cwtarget:
       
   213             attrs = {'name': form.cwtarget, 'id': form.cwtarget,
       
   214                      'width': '0px', 'height': '0px',
       
   215                      'src': 'javascript: void(0);'}
       
   216             out =  (u'<iframe %s></iframe>\n' % uilib.sgml_attributes(attrs)) + out
       
   217         return out
       
   218 
       
   219     def render_fields(self, w, form, values):
       
   220         fields = self._render_hidden_fields(w, form)
       
   221         if fields:
       
   222             self._render_fields(fields, w, form)
       
   223         self.render_child_forms(w, form, values)
       
   224 
       
   225     def render_child_forms(self, w, form, values):
       
   226         # render
       
   227         for childform in getattr(form, 'forms', []):
       
   228             self.render_fields(w, childform, values)
       
   229 
       
   230     def _render_hidden_fields(self, w, form):
       
   231         fields = form.fields[:]
       
   232         for field in form.fields:
       
   233             if not field.is_visible():
       
   234                 w(field.render(form, self))
       
   235                 w(u'\n')
       
   236                 fields.remove(field)
       
   237         return fields
       
   238 
       
   239     def _render_fields(self, fields, w, form):
       
   240         byfieldset = {}
       
   241         for field in fields:
       
   242             byfieldset.setdefault(field.fieldset, []).append(field)
       
   243         if form.fieldsets_in_order:
       
   244             fieldsets = form.fieldsets_in_order
       
   245         else:
       
   246             fieldsets = byfieldset
       
   247         for fieldset in list(fieldsets):
       
   248             try:
       
   249                 fields = byfieldset.pop(fieldset)
       
   250             except KeyError:
       
   251                 self.warning('no such fieldset: %s (%s)', fieldset, form)
       
   252                 continue
       
   253             w(u'<fieldset>\n')
       
   254             if fieldset:
       
   255                 w(u'<legend>%s</legend>' % self._cw.__(fieldset))
       
   256             w(u'<table class="%s">\n' % self.table_class)
       
   257             for field in fields:
       
   258                 w(u'<tr class="%s_%s_row">\n' % (field.name, field.role))
       
   259                 if self.display_label and field.label is not None:
       
   260                     w(u'<th class="labelCol">%s</th>\n' % self.render_label(form, field))
       
   261                 w(u'<td')
       
   262                 if field.label is None:
       
   263                     w(u' colspan="2"')
       
   264                 error = form.field_error(field)
       
   265                 if error:
       
   266                     w(u' class="error"')
       
   267                 w(u'>\n')
       
   268                 w(field.render(form, self))
       
   269                 w(u'\n')
       
   270                 if error:
       
   271                     self.render_error(w, error)
       
   272                 if self.display_help:
       
   273                     w(self.render_help(form, field))
       
   274                 w(u'</td></tr>\n')
       
   275             w(u'</table></fieldset>\n')
       
   276         if byfieldset:
       
   277             self.warning('unused fieldsets: %s', ', '.join(byfieldset))
       
   278 
       
   279     def render_buttons(self, w, form):
       
   280         if not form.form_buttons:
       
   281             return
       
   282         w(u'<table class="%s">\n<tr>\n' % self.button_bar_class)
       
   283         for button in form.form_buttons:
       
   284             w(u'<td>%s</td>\n' % button.render(form))
       
   285         w(u'</tr></table>')
       
   286 
       
   287     def render_error(self, w, err):
       
   288         """return validation error for widget's field, if any"""
       
   289         w(u'<span class="errorMsg">%s</span>' % err)
       
   290 
       
   291 
       
   292 
       
   293 class BaseFormRenderer(FormRenderer):
       
   294     """use form_renderer_id = 'base' if you want base FormRenderer layout even
       
   295     when selected for an entity
       
   296     """
       
   297     __regid__ = 'base'
       
   298 
       
   299 
       
   300 
       
   301 class HTableFormRenderer(FormRenderer):
       
   302     """The 'htable' form renderer display fields horizontally in a table:
       
   303 
       
   304     +--------------+--------------+---------+
       
   305     | field1 label | field2 label |         |
       
   306     +--------------+--------------+---------+
       
   307     | field1 input | field2 input | buttons |
       
   308     +--------------+--------------+---------+
       
   309     """
       
   310     __regid__ = 'htable'
       
   311 
       
   312     display_help = False
       
   313     def _render_fields(self, fields, w, form):
       
   314         w(u'<table border="0" class="htableForm">')
       
   315         w(u'<tr>')
       
   316         for field in fields:
       
   317             if self.display_label:
       
   318                 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
       
   319             if self.display_help:
       
   320                 w(self.render_help(form, field))
       
   321         # empty slot for buttons
       
   322         w(u'<th class="labelCol">&#160;</th>')
       
   323         w(u'</tr>')
       
   324         w(u'<tr>')
       
   325         for field in fields:
       
   326             error = form.field_error(field)
       
   327             if error:
       
   328                 w(u'<td class="error">')
       
   329                 self.render_error(w, error)
       
   330             else:
       
   331                 w(u'<td>')
       
   332             w(field.render(form, self))
       
   333             w(u'</td>')
       
   334         w(u'<td>')
       
   335         for button in form.form_buttons:
       
   336             w(button.render(form))
       
   337         w(u'</td>')
       
   338         w(u'</tr>')
       
   339         w(u'</table>')
       
   340 
       
   341     def render_buttons(self, w, form):
       
   342         pass
       
   343 
       
   344 
       
   345 class OneRowTableFormRenderer(FormRenderer):
       
   346     """The 'htable' form renderer display fields horizontally in a table:
       
   347 
       
   348     +--------------+--------------+--------------+--------------+---------+
       
   349     | field1 label | field1 input | field2 label | field2 input | buttons |
       
   350     +--------------+--------------+--------------+--------------+---------+
       
   351     """
       
   352     __regid__ = 'onerowtable'
       
   353 
       
   354     display_help = False
       
   355     def _render_fields(self, fields, w, form):
       
   356         w(u'<table border="0" class="oneRowTableForm">')
       
   357         w(u'<tr>')
       
   358         for field in fields:
       
   359             if self.display_label:
       
   360                 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
       
   361             if self.display_help:
       
   362                 w(self.render_help(form, field))
       
   363             error = form.field_error(field)
       
   364             if error:
       
   365                 w(u'<td class="error">')
       
   366                 self.render_error(w, error)
       
   367             else:
       
   368                 w(u'<td>')
       
   369             w(field.render(form, self))
       
   370             w(u'</td>')
       
   371         w(u'<td>')
       
   372         for button in form.form_buttons:
       
   373             w(button.render(form))
       
   374         w(u'</td>')
       
   375         w(u'</tr>')
       
   376         w(u'</table>')
       
   377 
       
   378     def render_buttons(self, w, form):
       
   379         pass
       
   380 
       
   381 
       
   382 class EntityCompositeFormRenderer(FormRenderer):
       
   383     """This is a specific renderer for the multiple entities edition form
       
   384     ('muledit').
       
   385 
       
   386     Each entity form will be displayed in row off a table, with a check box for
       
   387     each entities to indicate which ones are edited. Those checkboxes should be
       
   388     automatically updated when something is edited.
       
   389     """
       
   390     __regid__ = 'composite'
       
   391 
       
   392     _main_display_fields = None
       
   393 
       
   394     def render_fields(self, w, form, values):
       
   395         if form.parent_form is None:
       
   396             w(u'<table class="listing">')
       
   397             # get fields from the first subform with something to display (we
       
   398             # may have subforms with nothing editable that will simply be
       
   399             # skipped later)
       
   400             for subform in form.forms:
       
   401                 subfields = [field for field in subform.fields
       
   402                              if field.is_visible()]
       
   403                 if subfields:
       
   404                     break
       
   405             if subfields:
       
   406                 # main form, display table headers
       
   407                 w(u'<tr class="header">')
       
   408                 w(u'<th align="left">%s</th>' %
       
   409                   tags.input(type='checkbox',
       
   410                              title=self._cw._('toggle check boxes'),
       
   411                              onclick="setCheckboxesState('eid', null, this.checked)"))
       
   412                 for field in subfields:
       
   413                     w(u'<th>%s</th>' % field_label(form, field))
       
   414                 w(u'</tr>')
       
   415         super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
       
   416         if form.parent_form is None:
       
   417             w(u'</table>')
       
   418             if self._main_display_fields:
       
   419                 super(EntityCompositeFormRenderer, self)._render_fields(
       
   420                     self._main_display_fields, w, form)
       
   421 
       
   422     def _render_fields(self, fields, w, form):
       
   423         if form.parent_form is not None:
       
   424             entity = form.edited_entity
       
   425             values = form.form_previous_values
       
   426             qeid = eid_param('eid', entity.eid)
       
   427             cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \
       
   428                          xml_escape(json_dumps(entity.eid))
       
   429             w(u'<tr class="%s">' % (entity.cw_row % 2 and u'even' or u'odd'))
       
   430             # XXX turn this into a widget used on the eid field
       
   431             w(u'<td>%s</td>' % checkbox('eid', entity.eid,
       
   432                                         checked=qeid in values))
       
   433             for field in fields:
       
   434                 error = form.field_error(field)
       
   435                 if error:
       
   436                     w(u'<td class="error">')
       
   437                     self.render_error(w, error)
       
   438                 else:
       
   439                     w(u'<td>')
       
   440                 if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox,
       
   441                                              fwdgs.Radio)):
       
   442                     field.widget.attrs['onchange'] = cbsetstate
       
   443                 elif isinstance(field.widget, fwdgs.Input):
       
   444                     field.widget.attrs['onkeypress'] = cbsetstate
       
   445                 # XXX else
       
   446                 w(u'<div>%s</div>' % field.render(form, self))
       
   447                 w(u'</td>\n')
       
   448             w(u'</tr>')
       
   449         else:
       
   450             self._main_display_fields = fields
       
   451 
       
   452 
       
   453 class EntityFormRenderer(BaseFormRenderer):
       
   454     """This is the 'default' renderer for entity's form.
       
   455 
       
   456     You can still use form_renderer_id = 'base' if you want base FormRenderer
       
   457     layout even when selected for an entity.
       
   458     """
       
   459     __regid__ = 'default'
       
   460     # needs some additional points in some case (XXX explain cases)
       
   461     __select__ = is_instance('Any') & yes()
       
   462 
       
   463     _options = FormRenderer._options + ('main_form_title',)
       
   464     main_form_title = _('main informations')
       
   465 
       
   466     def open_form(self, form, values):
       
   467         attrs_fs_label = ''
       
   468         if self.main_form_title:
       
   469             attrs_fs_label += ('<div class="iformTitle"><span>%s</span></div>'
       
   470                                % self._cw._(self.main_form_title))
       
   471         attrs_fs_label += '<div class="formBody">'
       
   472         return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
       
   473 
       
   474     def close_form(self, form, values):
       
   475         """seems dumb but important for consistency w/ close form, and necessary
       
   476         for form renderers overriding open_form to use something else or more than
       
   477         and <form>
       
   478         """
       
   479         return super(EntityFormRenderer, self).close_form(form, values) + '</div>'
       
   480 
       
   481     def render_buttons(self, w, form):
       
   482         if len(form.form_buttons) == 3:
       
   483             w("""<table width="100%%">
       
   484   <tbody>
       
   485    <tr><td align="center">
       
   486      %s
       
   487    </td><td style="align: right; width: 50%%;">
       
   488      %s
       
   489      %s
       
   490    </td></tr>
       
   491   </tbody>
       
   492  </table>""" % tuple(button.render(form) for button in form.form_buttons))
       
   493         else:
       
   494             super(EntityFormRenderer, self).render_buttons(w, form)
       
   495 
       
   496 
       
   497 class EntityInlinedFormRenderer(EntityFormRenderer):
       
   498     """This is a specific renderer for entity's form inlined into another
       
   499     entity's form.
       
   500     """
       
   501     __regid__ = 'inline'
       
   502     fieldset_css_class = 'subentity'
       
   503 
       
   504     def render_title(self, w, form, values):
       
   505         w(u'<div class="iformTitle">')
       
   506         w(u'<span>%(title)s</span> '
       
   507           '#<span class="icounter">%(counter)s</span> ' % values)
       
   508         if values['removejs']:
       
   509             values['removemsg'] = self._cw._('remove-inlined-entity-form')
       
   510             w(u'[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]'
       
   511               % values)
       
   512         w(u'</div>')
       
   513 
       
   514     def render(self, w, form, values):
       
   515         form.add_media()
       
   516         self.open_form(w, form, values)
       
   517         self.render_title(w, form, values)
       
   518         # XXX that stinks
       
   519         # cleanup values
       
   520         for key in ('title', 'removejs', 'removemsg'):
       
   521             values.pop(key, None)
       
   522         self.render_fields(w, form, values)
       
   523         self.close_form(w, form, values)
       
   524 
       
   525     def open_form(self, w, form, values):
       
   526         try:
       
   527             w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
       
   528         except KeyError:
       
   529             w(u'<div id="div-%(divid)s">' % values)
       
   530         else:
       
   531             w(u'<div id="notice-%s" class="notice">%s</div>' % (
       
   532                 values['divid'], self._cw._('click on the box to cancel the deletion')))
       
   533         w(u'<div class="iformBody">')
       
   534 
       
   535     def close_form(self, w, form, values):
       
   536         w(u'</div></div>')
       
   537 
       
   538     def render_fields(self, w, form, values):
       
   539         w(u'<fieldset id="fs-%(divid)s">' % values)
       
   540         fields = self._render_hidden_fields(w, form)
       
   541         w(u'</fieldset>')
       
   542         w(u'<fieldset class="%s">' % self.fieldset_css_class)
       
   543         if fields:
       
   544             self._render_fields(fields, w, form)
       
   545         self.render_child_forms(w, form, values)
       
   546         w(u'</fieldset>')