web/views/formrenderers.py
changeset 1995 ec95eaa2b711
parent 1977 606923dff11b
child 2005 e8032965f37a
equal deleted inserted replaced
1994:56a235af050e 1995:ec95eaa2b711
       
     1 """form renderers, responsible to layout a form to html
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from logilab.common import dictattr
       
    11 from logilab.mtconverter import html_escape
       
    12 
       
    13 from simplejson import dumps
       
    14 
       
    15 from cubicweb.common import tags
       
    16 from cubicweb.appobject import AppRsetObject
       
    17 from cubicweb.selectors import entity_implements, yes
       
    18 from cubicweb.web import eid_param
       
    19 from cubicweb.web import formwidgets as fwdgs
       
    20 from cubicweb.web.widgets import checkbox
       
    21 from cubicweb.web.formfields import HiddenInitialValueField
       
    22 
       
    23 
       
    24 class FormRenderer(AppRsetObject):
       
    25     """basic renderer displaying fields in a two columns table label | value
       
    26 
       
    27     +--------------+--------------+
       
    28     | field1 label | field1 input |
       
    29     +--------------+--------------+
       
    30     | field1 label | field2 input |
       
    31     +--------------+--------------+
       
    32     +---------+
       
    33     | buttons |
       
    34     +---------+
       
    35     """
       
    36     __registry__ = 'formrenderers'
       
    37     id = 'default'
       
    38 
       
    39     _options = ('display_fields', 'display_label', 'display_help',
       
    40                 'display_progress_div', 'table_class', 'button_bar_class',
       
    41                 # add entity since it may be given to select the renderer
       
    42                 'entity')
       
    43     display_fields = None # None -> all fields
       
    44     display_label = True
       
    45     display_help = True
       
    46     display_progress_div = True
       
    47     table_class = u'attributeForm'
       
    48     button_bar_class = u'formButtonBar'
       
    49 
       
    50     def __init__(self, req=None, rset=None, row=None, col=None, **kwargs):
       
    51         super(FormRenderer, self).__init__(req, rset, row, col)
       
    52         if self._set_options(kwargs):
       
    53             raise ValueError('unconsumed arguments %s' % kwargs)
       
    54 
       
    55     def _set_options(self, kwargs):
       
    56         for key in self._options:
       
    57             try:
       
    58                 setattr(self, key, kwargs.pop(key))
       
    59             except KeyError:
       
    60                 continue
       
    61         return kwargs
       
    62 
       
    63     # renderer interface ######################################################
       
    64 
       
    65     def render(self, form, values):
       
    66         self._set_options(values)
       
    67         form.add_media()
       
    68         data = []
       
    69         w = data.append
       
    70         w(self.open_form(form, values))
       
    71         if self.display_progress_div:
       
    72             w(u'<div id="progress">%s</div>' % form.req._('validating...'))
       
    73         w(u'<fieldset>')
       
    74         w(tags.input(type=u'hidden', name=u'__form_id',
       
    75                      value=values.get('formvid', form.id)))
       
    76         if form.redirect_path:
       
    77             w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
       
    78         self.render_fields(w, form, values)
       
    79         self.render_buttons(w, form)
       
    80         w(u'</fieldset>')
       
    81         w(u'</form>')
       
    82         errormsg = self.error_message(form)
       
    83         if errormsg:
       
    84             data.insert(0, errormsg)
       
    85         return '\n'.join(data)
       
    86 
       
    87     def render_label(self, form, field):
       
    88         label = form.req._(field.label)
       
    89         attrs = {'for': form.context[field]['id']}
       
    90         if field.required:
       
    91             attrs['class'] = 'required'
       
    92         return tags.label(label, **attrs)
       
    93 
       
    94     def render_help(self, form, field):
       
    95         help = []
       
    96         descr = field.help
       
    97         if descr:
       
    98             help.append('<div class="helper">%s</div>' % form.req._(descr))
       
    99         example = field.example_format(form.req)
       
   100         if example:
       
   101             help.append('<div class="helper">(%s: %s)</div>'
       
   102                         % (form.req._('sample format'), example))
       
   103         return u'&nbsp;'.join(help)
       
   104 
       
   105     # specific methods (mostly to ease overriding) #############################
       
   106 
       
   107     def error_message(self, form):
       
   108         """return formatted error message
       
   109 
       
   110         This method should be called once inlined field errors has been consumed
       
   111         """
       
   112         req = form.req
       
   113         errex = form.form_valerror
       
   114         # get extra errors
       
   115         if errex is not None:
       
   116             errormsg = req._('please correct the following errors:')
       
   117             displayed = form.form_displayed_errors
       
   118             errors = sorted((field, err) for field, err in errex.errors.items()
       
   119                             if not field in displayed)
       
   120             if errors:
       
   121                 if len(errors) > 1:
       
   122                     templstr = '<li>%s</li>\n'
       
   123                 else:
       
   124                     templstr = '&nbsp;%s\n'
       
   125                 for field, err in errors:
       
   126                     if field is None:
       
   127                         errormsg += templstr % err
       
   128                     else:
       
   129                         errormsg += templstr % '%s: %s' % (req._(field), err)
       
   130                 if len(errors) > 1:
       
   131                     errormsg = '<ul>%s</ul>' % errormsg
       
   132             return u'<div class="errorMessage">%s</div>' % errormsg
       
   133         return u''
       
   134 
       
   135     def open_form(self, form, values):
       
   136         if form.form_needs_multipart:
       
   137             enctype = 'multipart/form-data'
       
   138         else:
       
   139             enctype = 'application/x-www-form-urlencoded'
       
   140         if form.action is None:
       
   141             action = form.req.build_url('edit')
       
   142         else:
       
   143             action = form.action
       
   144         tag = ('<form action="%s" method="post" enctype="%s"' % (
       
   145             html_escape(action or '#'), enctype))
       
   146         if form.domid:
       
   147             tag += ' id="%s"' % form.domid
       
   148         if form.onsubmit:
       
   149             tag += ' onsubmit="%s"' % html_escape(form.onsubmit % dictattr(form))
       
   150         if form.cssstyle:
       
   151             tag += ' style="%s"' % html_escape(form.cssstyle)
       
   152         if form.cssclass:
       
   153             tag += ' class="%s"' % html_escape(form.cssclass)
       
   154         if form.cwtarget:
       
   155             tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
       
   156         return tag + '>'
       
   157 
       
   158     def display_field(self, form, field):
       
   159         if isinstance(field, HiddenInitialValueField):
       
   160             field = field.visible_field
       
   161         return (self.display_fields is None
       
   162                 or field.name in form.internal_fields
       
   163                 or (field.name, field.role) in self.display_fields
       
   164                 or (field.name, field.role) in form.internal_fields)
       
   165 
       
   166     def render_fields(self, w, form, values):
       
   167         form.form_build_context(values)
       
   168         fields = self._render_hidden_fields(w, form)
       
   169         if fields:
       
   170             self._render_fields(fields, w, form)
       
   171         self.render_child_forms(w, form, values)
       
   172 
       
   173     def render_child_forms(self, w, form, values):
       
   174         # render
       
   175         for childform in getattr(form, 'forms', []):
       
   176             self.render_fields(w, childform, values)
       
   177 
       
   178     def _render_hidden_fields(self, w, form):
       
   179         fields = form.fields[:]
       
   180         for field in form.fields:
       
   181             if not self.display_field(form, field):
       
   182                 fields.remove(field)
       
   183             elif not field.is_visible():
       
   184                 w(field.render(form, self))
       
   185                 fields.remove(field)
       
   186         return fields
       
   187 
       
   188     def _render_fields(self, fields, w, form):
       
   189         w(u'<table class="%s">' % self.table_class)
       
   190         for field in fields:
       
   191             w(u'<tr>')
       
   192             if self.display_label:
       
   193                 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
       
   194             error = form.form_field_error(field)
       
   195             if error:
       
   196                 w(u'<td class="error">')
       
   197                 w(error)
       
   198             else:
       
   199                 w(u'<td>')
       
   200             w(field.render(form, self))
       
   201             if self.display_help:
       
   202                 w(self.render_help(form, field))
       
   203             w(u'</td></tr>')
       
   204         w(u'</table>')
       
   205 
       
   206     def render_buttons(self, w, form):
       
   207         w(u'<table class="%s">\n<tr>\n' % self.button_bar_class)
       
   208         for button in form.form_buttons:
       
   209             w(u'<td>%s</td>\n' % button.render(form))
       
   210         w(u'</tr></table>')
       
   211 
       
   212 
       
   213 class HTableFormRenderer(FormRenderer):
       
   214     """display fields horizontally in a table
       
   215 
       
   216     +--------------+--------------+---------+
       
   217     | field1 label | field2 label |         |
       
   218     +--------------+--------------+---------+
       
   219     | field1 input | field2 input | buttons
       
   220     +--------------+--------------+---------+
       
   221     """
       
   222     id = 'htable'
       
   223 
       
   224     display_help = False
       
   225     def _render_fields(self, fields, w, form):
       
   226         w(u'<table border="0">')
       
   227         w(u'<tr>')
       
   228         for field in fields:
       
   229             if self.display_label:
       
   230                 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
       
   231             if self.display_help:
       
   232                 w(self.render_help(form, field))
       
   233         # empty slot for buttons
       
   234         w(u'<th class="labelCol">&nbsp;</th>')
       
   235         w(u'</tr>')
       
   236         w(u'<tr>')
       
   237         for field in fields:
       
   238             error = form.form_field_error(field)
       
   239             if error:
       
   240                 w(u'<td class="error">')
       
   241                 w(error)
       
   242             else:
       
   243                 w(u'<td>')
       
   244             w(field.render(form, self))
       
   245             w(u'</td>')
       
   246         w(u'<td>')
       
   247         for button in form.form_buttons:
       
   248             w(button.render(form))
       
   249         w(u'</td>')
       
   250         w(u'</tr>')
       
   251         w(u'</table>')
       
   252 
       
   253     def render_buttons(self, w, form):
       
   254         pass
       
   255 
       
   256 
       
   257 class EntityCompositeFormRenderer(FormRenderer):
       
   258     """specific renderer for multiple entities edition form (muledit)"""
       
   259     id = 'composite'
       
   260 
       
   261     def render_fields(self, w, form, values):
       
   262         if not form.is_subform:
       
   263             w(u'<table class="listing">')
       
   264         super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
       
   265         if not form.is_subform:
       
   266             w(u'</table>')
       
   267 
       
   268     def _render_fields(self, fields, w, form):
       
   269         if form.is_subform:
       
   270             entity = form.edited_entity
       
   271             values = form.form_previous_values
       
   272             qeid = eid_param('eid', entity.eid)
       
   273             cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid))
       
   274             w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd'))
       
   275             # XXX turn this into a widget used on the eid field
       
   276             w(u'<td>%s</td>' % checkbox('eid', entity.eid, checked=qeid in values))
       
   277             for field in fields:
       
   278                 error = form.form_field_error(field)
       
   279                 if error:
       
   280                     w(u'<td class="error">')
       
   281                     w(error)
       
   282                 else:
       
   283                     w(u'<td>')
       
   284                 if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)):
       
   285                     field.widget.attrs['onchange'] = cbsetstate
       
   286                 elif isinstance(field.widget, fwdgs.Input):
       
   287                     field.widget.attrs['onkeypress'] = cbsetstate
       
   288                 w(u'<div>%s</div>' % field.render(form, self))
       
   289                 w(u'</td>')
       
   290         else:
       
   291             # main form, display table headers
       
   292             w(u'<tr class="header">')
       
   293             w(u'<th align="left">%s</th>'
       
   294               % tags.input(type='checkbox', title=form.req._('toggle check boxes'),
       
   295                            onclick="setCheckboxesState('eid', this.checked)"))
       
   296             for field in self.forms[0].fields:
       
   297                 if self.display_field(form, field) and field.is_visible():
       
   298                     w(u'<th>%s</th>' % form.req._(field.label))
       
   299         w(u'</tr>')
       
   300 
       
   301 class BaseFormRenderer(FormRenderer):
       
   302     """use form_renderer_id = 'base' if you don't want adaptation by selection
       
   303     """
       
   304     id = 'base'
       
   305 
       
   306 class EntityFormRenderer(FormRenderer):
       
   307     """specific renderer for entity edition form (edition)"""
       
   308     __select__ = entity_implements('Any') & yes()
       
   309 
       
   310     _options = FormRenderer._options + ('display_relations_form',)
       
   311     display_relations_form = True
       
   312 
       
   313     def render(self, form, values):
       
   314         rendered = super(EntityFormRenderer, self).render(form, values)
       
   315         return rendered + u'</div>' # close extra div introducted by open_form
       
   316 
       
   317     def open_form(self, form, values):
       
   318         attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>'
       
   319                           % form.req._('main informations'))
       
   320         attrs_fs_label += '<div class="formBody">'
       
   321         return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
       
   322 
       
   323     def render_fields(self, w, form, values):
       
   324         super(EntityFormRenderer, self).render_fields(w, form, values)
       
   325         self.inline_entities_form(w, form)
       
   326         if form.edited_entity.has_eid() and self.display_relations_form:
       
   327             self.relations_form(w, form)
       
   328 
       
   329     def _render_fields(self, fields, w, form):
       
   330         if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'):
       
   331             super(EntityFormRenderer, self)._render_fields(fields, w, form)
       
   332 
       
   333     def render_buttons(self, w, form):
       
   334         if len(form.form_buttons) == 3:
       
   335             w("""<table width="100%%">
       
   336   <tbody>
       
   337    <tr><td align="center">
       
   338      %s
       
   339    </td><td style="align: right; width: 50%%;">
       
   340      %s
       
   341      %s
       
   342    </td></tr>
       
   343   </tbody>
       
   344  </table>""" % tuple(button.render(form) for button in form.form_buttons))
       
   345         else:
       
   346             super(EntityFormRenderer, self).render_buttons(w, form)
       
   347 
       
   348     def relations_form(self, w, form):
       
   349         srels_by_cat = form.srelations_by_category('generic', 'add')
       
   350         if not srels_by_cat:
       
   351             return u''
       
   352         req = form.req
       
   353         _ = req._
       
   354         label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize()
       
   355         eid = form.edited_entity.eid
       
   356         w(u'<fieldset class="subentity">')
       
   357         w(u'<legend class="iformTitle">%s</legend>' % label)
       
   358         w(u'<table id="relatedEntities">')
       
   359         for rschema, target, related in form.relations_table():
       
   360             # already linked entities
       
   361             if related:
       
   362                 w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target))
       
   363                 w(u'<td>')
       
   364                 w(u'<ul>')
       
   365                 for viewparams in related:
       
   366                     w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
       
   367                       % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
       
   368                 if not form.force_display and form.maxrelitems < len(related):
       
   369                     link = (u'<span class="invisible">'
       
   370                             '[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
       
   371                             '</span>' % form.req._('view all'))
       
   372                     w(u'<li class="invisible">%s</li>' % link)
       
   373                 w(u'</ul>')
       
   374                 w(u'</td>')
       
   375                 w(u'</tr>')
       
   376         pendings = list(form.restore_pending_inserts())
       
   377         if not pendings:
       
   378             w(u'<tr><th>&nbsp;</th><td>&nbsp;</td></tr>')
       
   379         else:
       
   380             for row in pendings:
       
   381                 # soon to be linked to entities
       
   382                 w(u'<tr id="tr%s">' % row[1])
       
   383                 w(u'<th>%s</th>' % row[3])
       
   384                 w(u'<td>')
       
   385                 w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
       
   386                   (_('cancel this insert'), row[2]))
       
   387                 w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
       
   388                   % (row[1], row[4], html_escape(row[5])))
       
   389                 w(u'</td>')
       
   390                 w(u'</tr>')
       
   391         w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
       
   392         w(u'<th class="labelCol">')
       
   393         w(u'<span>%s</span>' % _('add relation'))
       
   394         w(u'<select id="relationSelector_%s" tabindex="%s" '
       
   395           'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
       
   396           % (eid, req.next_tabindex(), html_escape(dumps(eid))))
       
   397         w(u'<option value="">%s</option>' % _('select a relation'))
       
   398         for i18nrtype, rschema, target in srels_by_cat:
       
   399             # more entities to link to
       
   400             w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype))
       
   401         w(u'</select>')
       
   402         w(u'</th>')
       
   403         w(u'<td id="unrelatedDivs_%s"></td>' % eid)
       
   404         w(u'</tr>')
       
   405         w(u'</table>')
       
   406         w(u'</fieldset>')
       
   407 
       
   408     def inline_entities_form(self, w, form):
       
   409         """create a form to edit entity's inlined relations"""
       
   410         if not hasattr(form, 'inlined_relations'):
       
   411             return
       
   412         entity = form.edited_entity
       
   413         __ = form.req.__
       
   414         for rschema, targettypes, role in form.inlined_relations():
       
   415             # show inline forms only if there's one possible target type
       
   416             # for rschema
       
   417             if len(targettypes) != 1:
       
   418                 self.warning('entity related by the %s relation should have '
       
   419                              'inlined form but there is multiple target types, '
       
   420                              'dunno what to do', rschema)
       
   421                 continue
       
   422             targettype = targettypes[0].type
       
   423             if form.should_inline_relation_form(rschema, targettype, role):
       
   424                 w(u'<div id="inline%sslot">' % rschema)
       
   425                 existant = entity.has_eid() and entity.related(rschema)
       
   426                 if existant:
       
   427                     # display inline-edition view for all existing related entities
       
   428                     w(form.view('inline-edition', existant, rtype=rschema, role=role,
       
   429                                 ptype=entity.e_schema, peid=entity.eid))
       
   430                 if role == 'subject':
       
   431                     card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0]
       
   432                 else:
       
   433                     card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1]
       
   434                 # there is no related entity and we need at least one: we need to
       
   435                 # display one explicit inline-creation view
       
   436                 if form.should_display_inline_creation_form(rschema, existant, card):
       
   437                     w(form.view('inline-creation', None, etype=targettype,
       
   438                                 peid=entity.eid, ptype=entity.e_schema,
       
   439                                 rtype=rschema, role=role))
       
   440                 # we can create more than one related entity, we thus display a link
       
   441                 # to add new related entities
       
   442                 if form.should_display_add_new_relation_link(rschema, existant, card):
       
   443                     divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid)
       
   444                     w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
       
   445                       % divid)
       
   446                     js = "addInlineCreationForm('%s', '%s', '%s', '%s')" % (
       
   447                         entity.eid, targettype, rschema, role)
       
   448                     if card in '1?':
       
   449                         js = "toggleVisibility('%s'); %s" % (divid, js)
       
   450                     w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
       
   451                       % (rschema, entity.eid, js, __('add a %s' % targettype)))
       
   452                     w(u'</div>')
       
   453                     w(u'<div class="trame_grise">&nbsp;</div>')
       
   454                 w(u'</div>')
       
   455 
       
   456 
       
   457 class EntityInlinedFormRenderer(EntityFormRenderer):
       
   458     """specific renderer for entity inlined edition form
       
   459     (inline-[creation|edition])
       
   460     """
       
   461     id = 'inline'
       
   462 
       
   463     def render(self, form, values):
       
   464         form.add_media()
       
   465         data = []
       
   466         w = data.append
       
   467         try:
       
   468             w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
       
   469         except KeyError:
       
   470             w(u'<div id="div-%(divid)s">' % values)
       
   471         else:
       
   472             w(u'<div id="notice-%s" class="notice">%s</div>' % (
       
   473                 values['divid'], form.req._('click on the box to cancel the deletion')))
       
   474         w(u'<div class="iformBody">')
       
   475         values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema)
       
   476         w(u'<div class="iformTitle"><span>%(title)s</span> '
       
   477           '#<span class="icounter">1</span> '
       
   478           '[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>'
       
   479           % values)
       
   480         # cleanup values
       
   481         for key in ('title', 'removejs', 'removemsg'):
       
   482             values.pop(key)
       
   483         self.render_fields(w, form, values)
       
   484         w(u'</div></div>')
       
   485         return '\n'.join(data)
       
   486 
       
   487     def render_fields(self, w, form, values):
       
   488         form.form_build_context(values)
       
   489         w(u'<fieldset id="fs-%(divid)s">' % values)
       
   490         fields = self._render_hidden_fields(w, form)
       
   491         w(u'</fieldset>')
       
   492         w(u'<fieldset class="subentity">')
       
   493         if fields:
       
   494             self._render_fields(fields, w, form)
       
   495         self.render_child_forms(w, form, values)
       
   496         self.inline_entities_form(w, form)
       
   497         w(u'</fieldset>')
       
   498