diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/formrenderers.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/formrenderers.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,546 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see .
+"""
+Renderers
+---------
+
+.. Note::
+ Form renderers are responsible to layout a form to HTML.
+
+Here are the base renderers available:
+
+.. autoclass:: cubicweb.web.views.formrenderers.FormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer
+
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from warnings import warn
+
+from six import text_type
+
+from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
+
+from cubicweb import tags, uilib
+from cubicweb.appobject import AppObject
+from cubicweb.predicates import is_instance
+from cubicweb.utils import json_dumps, support_args
+from cubicweb.web import eid_param, formwidgets as fwdgs
+
+
+def checkbox(name, value, attrs='', checked=None):
+ if checked is None:
+ checked = value
+ checked = checked and 'checked="checked"' or ''
+ return u'' % (
+ name, value, checked, attrs)
+
+def field_label(form, field):
+ if callable(field.label):
+ return field.label(form, field)
+ # XXX with 3.6 we can now properly rely on 'if field.role is not None' and
+ # stop having a tuple for label
+ if isinstance(field.label, tuple): # i.e. needs contextual translation
+ return form._cw.pgettext(*field.label)
+ return form._cw._(field.label)
+
+
+
+class FormRenderer(AppObject):
+ """This is the 'default' renderer, displaying fields in a two columns table:
+
+ +--------------+--------------+
+ | field1 label | field1 input |
+ +--------------+--------------+
+ | field2 label | field2 input |
+ +--------------+--------------+
+
+ +---------+
+ | buttons |
+ +---------+
+ """
+ __registry__ = 'formrenderers'
+ __regid__ = 'default'
+
+ _options = ('display_label', 'display_help',
+ 'display_progress_div', 'table_class', 'button_bar_class',
+ # add entity since it may be given to select the renderer
+ 'entity')
+ display_label = True
+ display_help = True
+ display_progress_div = True
+ table_class = u'attributeForm'
+ button_bar_class = u'formButtonBar'
+
+ def __init__(self, req=None, rset=None, row=None, col=None, **kwargs):
+ super(FormRenderer, self).__init__(req, rset=rset, row=row, col=col)
+ if self._set_options(kwargs):
+ raise ValueError('unconsumed arguments %s' % kwargs)
+
+ def _set_options(self, kwargs):
+ for key in self._options:
+ try:
+ setattr(self, key, kwargs.pop(key))
+ except KeyError:
+ continue
+ return kwargs
+
+ # renderer interface ######################################################
+
+ def render(self, w, form, values):
+ self._set_options(values)
+ form.add_media()
+ data = []
+ _w = data.append
+ _w(self.open_form(form, values))
+ self.render_content(_w, form, values)
+ _w(self.close_form(form, values))
+ errormsg = self.error_message(form)
+ if errormsg:
+ data.insert(0, errormsg)
+ # NOTE: we call unicode because `tag` objects may be found within data
+ # e.g. from the cwtags library
+ w(''.join(text_type(x) for x in data))
+
+ def render_content(self, w, form, values):
+ if self.display_progress_div:
+ w(u'
' % self._cw._(descr))
+ example = field.example_format(self._cw)
+ if example:
+ help.append('
(%s: %s)
'
+ % (self._cw._('sample format'), example))
+ return u' '.join(help)
+
+ # specific methods (mostly to ease overriding) #############################
+
+ def error_message(self, form):
+ """return formatted error message
+
+ This method should be called once inlined field errors has been consumed
+ """
+ req = self._cw
+ errex = form.form_valerror
+ # get extra errors
+ if errex is not None:
+ errormsg = req._('please correct the following errors:')
+ errors = form.remaining_errors()
+ if errors:
+ if len(errors) > 1:
+ templstr = u'
%s
\n'
+ else:
+ templstr = u' %s\n'
+ for field, err in errors:
+ if field is None:
+ errormsg += templstr % err
+ else:
+ errormsg += templstr % '%s: %s' % (req._(field), err)
+ if len(errors) > 1:
+ errormsg = '
%s
' % errormsg
+ return u'
%s
' % errormsg
+ return u''
+
+ def open_form(self, form, values, **attrs):
+ if form.needs_multipart:
+ enctype = u'multipart/form-data'
+ else:
+ enctype = u'application/x-www-form-urlencoded'
+ attrs.setdefault('enctype', enctype)
+ attrs.setdefault('method', 'post')
+ attrs.setdefault('action', form.form_action() or '#')
+ if form.domid:
+ attrs.setdefault('id', form.domid)
+ if form.onsubmit:
+ attrs.setdefault('onsubmit', form.onsubmit)
+ if form.cssstyle:
+ attrs.setdefault('style', form.cssstyle)
+ if form.cssclass:
+ attrs.setdefault('class', form.cssclass)
+ if form.cwtarget:
+ attrs.setdefault('target', form.cwtarget)
+ if not form.autocomplete:
+ attrs.setdefault('autocomplete', 'off')
+ return ''
+ if form.cwtarget:
+ attrs = {'name': form.cwtarget, 'id': form.cwtarget,
+ 'width': '0px', 'height': '0px',
+ 'src': 'javascript: void(0);'}
+ out = (u'\n' % uilib.sgml_attributes(attrs)) + out
+ return out
+
+ def render_fields(self, w, form, values):
+ fields = self._render_hidden_fields(w, form)
+ if fields:
+ self._render_fields(fields, w, form)
+ self.render_child_forms(w, form, values)
+
+ def render_child_forms(self, w, form, values):
+ # render
+ for childform in getattr(form, 'forms', []):
+ self.render_fields(w, childform, values)
+
+ def _render_hidden_fields(self, w, form):
+ fields = form.fields[:]
+ for field in form.fields:
+ if not field.is_visible():
+ w(field.render(form, self))
+ w(u'\n')
+ fields.remove(field)
+ return fields
+
+ def _render_fields(self, fields, w, form):
+ byfieldset = {}
+ for field in fields:
+ byfieldset.setdefault(field.fieldset, []).append(field)
+ if form.fieldsets_in_order:
+ fieldsets = form.fieldsets_in_order
+ else:
+ fieldsets = byfieldset
+ for fieldset in list(fieldsets):
+ try:
+ fields = byfieldset.pop(fieldset)
+ except KeyError:
+ self.warning('no such fieldset: %s (%s)', fieldset, form)
+ continue
+ w(u'\n')
+ if byfieldset:
+ self.warning('unused fieldsets: %s', ', '.join(byfieldset))
+
+ def render_buttons(self, w, form):
+ if not form.form_buttons:
+ return
+ w(u'
\n
\n' % self.button_bar_class)
+ for button in form.form_buttons:
+ w(u'
%s
\n' % button.render(form))
+ w(u'
')
+
+ def render_error(self, w, err):
+ """return validation error for widget's field, if any"""
+ w(u'%s' % err)
+
+
+
+class BaseFormRenderer(FormRenderer):
+ """use form_renderer_id = 'base' if you want base FormRenderer layout even
+ when selected for an entity
+ """
+ __regid__ = 'base'
+
+
+
+class HTableFormRenderer(FormRenderer):
+ """The 'htable' form renderer display fields horizontally in a table:
+
+ +--------------+--------------+---------+
+ | field1 label | field2 label | |
+ +--------------+--------------+---------+
+ | field1 input | field2 input | buttons |
+ +--------------+--------------+---------+
+ """
+ __regid__ = 'htable'
+
+ display_help = False
+ def _render_fields(self, fields, w, form):
+ w(u'
')
+ w(u'
')
+ for field in fields:
+ if self.display_label:
+ w(u'
%s
' % self.render_label(form, field))
+ if self.display_help:
+ w(self.render_help(form, field))
+ # empty slot for buttons
+ w(u'
')
+ w(u'
')
+ w(u'
')
+ for field in fields:
+ error = form.field_error(field)
+ if error:
+ w(u'
')
+ self.render_error(w, error)
+ else:
+ w(u'
')
+ w(field.render(form, self))
+ w(u'
')
+ w(u'
')
+ for button in form.form_buttons:
+ w(button.render(form))
+ w(u'
')
+ for field in fields:
+ if self.display_label:
+ w(u'
%s
' % self.render_label(form, field))
+ if self.display_help:
+ w(self.render_help(form, field))
+ error = form.field_error(field)
+ if error:
+ w(u'
')
+ self.render_error(w, error)
+ else:
+ w(u'
')
+ w(field.render(form, self))
+ w(u'
')
+ w(u'
')
+ for button in form.form_buttons:
+ w(button.render(form))
+ w(u'
')
+ w(u'
')
+ w(u'
')
+
+ def render_buttons(self, w, form):
+ pass
+
+
+class EntityCompositeFormRenderer(FormRenderer):
+ """This is a specific renderer for the multiple entities edition form
+ ('muledit').
+
+ Each entity form will be displayed in row off a table, with a check box for
+ each entities to indicate which ones are edited. Those checkboxes should be
+ automatically updated when something is edited.
+ """
+ __regid__ = 'composite'
+
+ _main_display_fields = None
+
+ def render_fields(self, w, form, values):
+ if form.parent_form is None:
+ w(u'
')
+ # get fields from the first subform with something to display (we
+ # may have subforms with nothing editable that will simply be
+ # skipped later)
+ for subform in form.forms:
+ subfields = [field for field in subform.fields
+ if field.is_visible()]
+ if subfields:
+ break
+ if subfields:
+ # main form, display table headers
+ w(u'
')
+ w(u'
%s
' %
+ tags.input(type='checkbox',
+ title=self._cw._('toggle check boxes'),
+ onclick="setCheckboxesState('eid', null, this.checked)"))
+ for field in subfields:
+ w(u'
%s
' % field_label(form, field))
+ w(u'
')
+ super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
+ if form.parent_form is None:
+ w(u'
')
+ else:
+ self._main_display_fields = fields
+
+
+class EntityFormRenderer(BaseFormRenderer):
+ """This is the 'default' renderer for entity's form.
+
+ You can still use form_renderer_id = 'base' if you want base FormRenderer
+ layout even when selected for an entity.
+ """
+ __regid__ = 'default'
+ # needs some additional points in some case (XXX explain cases)
+ __select__ = is_instance('Any') & yes()
+
+ _options = FormRenderer._options + ('main_form_title',)
+ main_form_title = _('main informations')
+
+ def open_form(self, form, values):
+ attrs_fs_label = ''
+ if self.main_form_title:
+ attrs_fs_label += ('
'
+ return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
+
+ def close_form(self, form, values):
+ """seems dumb but important for consistency w/ close form, and necessary
+ for form renderers overriding open_form to use something else or more than
+ and