# copyright 2003-2011 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 <http://www.gnu.org/licenses/>."""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"_=unicodefromwarningsimportwarnfromlogilab.mtconverterimportxml_escapefromcubicwebimporttags,uilibfromcubicweb.appobjectimportAppObjectfromcubicweb.selectorsimportis_instance,yesfromcubicweb.utilsimportjson_dumps,support_argsfromcubicweb.webimporteid_param,formwidgetsasfwdgsdefcheckbox(name,value,attrs='',checked=None):ifcheckedisNone:checked=valuechecked=checkedand'checked="checked"'or''returnu'<input type="checkbox" name="%s" value="%s" %s%s />'%(name,value,checked,attrs)deffield_label(form,field):ifcallable(field.label):returnfield.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 labelifisinstance(field.label,tuple):# i.e. needs contextual translationreturnform._cw.pgettext(*field.label)returnform._cw._(field.label)classFormRenderer(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=Truedisplay_help=Truedisplay_progress_div=Truetable_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)ifself._set_options(kwargs):raiseValueError('unconsumed arguments %s'%kwargs)def_set_options(self,kwargs):forkeyinself._options:try:setattr(self,key,kwargs.pop(key))exceptKeyError:continuereturnkwargs# renderer interface ######################################################defrender(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)iferrormsg:data.insert(0,errormsg)w(''.join(data))defrender_content(self,w,form,values):ifself.display_progress_div:w(u'<div id="progress">%s</div>'%self._cw._('validating...'))w(u'\n<fieldset>\n')self.render_fields(w,form,values)self.render_buttons(w,form)w(u'\n</fieldset>\n')defrender_label(self,form,field):iffield.labelisNone:returnu''label=field_label(form,field)attrs={'for':field.dom_id(form)}iffield.required:attrs['class']='required'returntags.label(label,**attrs)defrender_help(self,form,field):help=[]descr=field.helpifcallable(descr):ifsupport_args(descr,'form','field'):descr=descr(form,field)else:warn("[3.10] field's help callback must now take form and field as argument (%s)"%field,DeprecationWarning)descr=descr(form)ifdescr:help.append('<div class="helper">%s</div>'%self._cw._(descr))example=field.example_format(self._cw)ifexample:help.append('<div class="helper">(%s: %s)</div>'%(self._cw._('sample format'),example))returnu' '.join(help)# specific methods (mostly to ease overriding) #############################deferror_message(self,form):"""return formatted error message This method should be called once inlined field errors has been consumed """req=self._cwerrex=form.form_valerror# get extra errorsiferrexisnotNone:errormsg=req._('please correct the following errors:')errors=form.remaining_errors()iferrors:iflen(errors)>1:templstr=u'<li>%s</li>\n'else:templstr=u' %s\n'forfield,errinerrors:iffieldisNone:errormsg+=templstr%errelse:errormsg+=templstr%'%s: %s'%(req._(field),err)iflen(errors)>1:errormsg='<ul>%s</ul>'%errormsgreturnu'<div class="errorMessage">%s</div>'%errormsgreturnu''defopen_form(self,form,values,**attrs):ifform.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'#')ifform.domid:attrs.setdefault('id',form.domid)ifform.onsubmit:attrs.setdefault('onsubmit',form.onsubmit)ifform.cssstyle:attrs.setdefault('style',form.cssstyle)ifform.cssclass:attrs.setdefault('class',form.cssclass)ifform.cwtarget:attrs.setdefault('cubicweb:target',form.cwtarget)return'<form %s>'%uilib.sgml_attributes(attrs)defclose_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 <form> """returnu'</form>'defrender_fields(self,w,form,values):fields=self._render_hidden_fields(w,form)iffields:self._render_fields(fields,w,form)self.render_child_forms(w,form,values)defrender_child_forms(self,w,form,values):# renderforchildformingetattr(form,'forms',[]):self.render_fields(w,childform,values)def_render_hidden_fields(self,w,form):fields=form.fields[:]forfieldinform.fields:ifnotfield.is_visible():w(field.render(form,self))w(u'\n')fields.remove(field)returnfieldsdef_render_fields(self,fields,w,form):byfieldset={}forfieldinfields:byfieldset.setdefault(field.fieldset,[]).append(field)ifform.fieldsets_in_order:fieldsets=form.fieldsets_in_orderelse:fieldsets=byfieldset.keys()forfieldsetinfieldsets:try:fields=byfieldset.pop(fieldset)exceptKeyError:self.warning('no such fieldset: %s (%s)',fieldset,form)continuew(u'<fieldset class="%s">\n'%(fieldsetoru'default'))iffieldset:w(u'<legend>%s</legend>'%self._cw._(fieldset))w(u'<table class="%s">\n'%self.table_class)forfieldinfields:w(u'<tr class="%s_%s_row">\n'%(field.name,field.role))ifself.display_labelandfield.labelisnotNone:w(u'<th class="labelCol">%s</th>\n'%self.render_label(form,field))w(u'<td')iffield.labelisNone:w(u' colspan="2"')error=form.field_error(field)iferror:w(u' class="error"')w(u'>\n')w(field.render(form,self))w(u'\n')iferror:self.render_error(w,error)ifself.display_help:w(self.render_help(form,field))w(u'</td></tr>\n')w(u'</table></fieldset>\n')ifbyfieldset:self.warning('unused fieldsets: %s',', '.join(byfieldset))defrender_buttons(self,w,form):ifnotform.form_buttons:returnw(u'<table class="%s">\n<tr>\n'%self.button_bar_class)forbuttoninform.form_buttons:w(u'<td>%s</td>\n'%button.render(form))w(u'</tr></table>')defrender_error(self,w,err):"""return validation error for widget's field, if any"""w(u'<span class="errorMsg">%s</span>'%err)classBaseFormRenderer(FormRenderer):"""use form_renderer_id = 'base' if you want base FormRenderer layout even when selected for an entity """__regid__='base'classHTableFormRenderer(FormRenderer):"""The 'htable' form renderer display fields horizontally in a table: +--------------+--------------+---------+ | field1 label | field2 label | | +--------------+--------------+---------+ | field1 input | field2 input | buttons | +--------------+--------------+---------+ """__regid__='htable'display_help=Falsedef_render_fields(self,fields,w,form):w(u'<table border="0" class="htableForm">')w(u'<tr>')forfieldinfields:ifself.display_label:w(u'<th class="labelCol">%s</th>'%self.render_label(form,field))ifself.display_help:w(self.render_help(form,field))# empty slot for buttonsw(u'<th class="labelCol"> </th>')w(u'</tr>')w(u'<tr>')forfieldinfields:error=form.field_error(field)iferror:w(u'<td class="error">')self.render_error(w,error)else:w(u'<td>')w(field.render(form,self))w(u'</td>')w(u'<td>')forbuttoninform.form_buttons:w(button.render(form))w(u'</td>')w(u'</tr>')w(u'</table>')defrender_buttons(self,w,form):passclassOneRowTableFormRenderer(FormRenderer):"""The 'htable' form renderer display fields horizontally in a table: +--------------+--------------+--------------+--------------+---------+ | field1 label | field1 input | field2 label | field2 input | buttons | +--------------+--------------+--------------+--------------+---------+ """__regid__='onerowtable'display_help=Falsedef_render_fields(self,fields,w,form):w(u'<table border="0" class="oneRowTableForm">')w(u'<tr>')forfieldinfields:ifself.display_label:w(u'<th class="labelCol">%s</th>'%self.render_label(form,field))ifself.display_help:w(self.render_help(form,field))error=form.field_error(field)iferror:w(u'<td class="error">')self.render_error(w,error)else:w(u'<td>')w(field.render(form,self))w(u'</td>')w(u'<td>')forbuttoninform.form_buttons:w(button.render(form))w(u'</td>')w(u'</tr>')w(u'</table>')defrender_buttons(self,w,form):passclassEntityCompositeFormRenderer(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=Nonedefrender_fields(self,w,form,values):ifform.parent_formisNone:w(u'<table class="listing">')# get fields from the first subform with something to display (we# may have subforms with nothing editable that will simply be# skipped later)forsubforminform.forms:subfields=[fieldforfieldinsubform.fieldsiffield.is_visible()]ifsubfields:breakifsubfields:# main form, display table headersw(u'<tr class="header">')w(u'<th align="left">%s</th>'%tags.input(type='checkbox',title=self._cw._('toggle check boxes'),onclick="setCheckboxesState('eid', null, this.checked)"))forfieldinsubfields:w(u'<th>%s</th>'%field_label(form,field))w(u'</tr>')super(EntityCompositeFormRenderer,self).render_fields(w,form,values)ifform.parent_formisNone:w(u'</table>')ifself._main_display_fields:super(EntityCompositeFormRenderer,self)._render_fields(self._main_display_fields,w,form)def_render_fields(self,fields,w,form):ifform.parent_formisnotNone:entity=form.edited_entityvalues=form.form_previous_valuesqeid=eid_param('eid',entity.eid)cbsetstate="setCheckboxesState('eid', %s, 'checked')"% \xml_escape(json_dumps(entity.eid))w(u'<tr class="%s">'%(entity.cw_row%2andu'even'oru'odd'))# XXX turn this into a widget used on the eid fieldw(u'<td>%s</td>'%checkbox('eid',entity.eid,checked=qeidinvalues))forfieldinfields:error=form.field_error(field)iferror:w(u'<td class="error">')self.render_error(w,error)else:w(u'<td>')ifisinstance(field.widget,(fwdgs.Select,fwdgs.CheckBox,fwdgs.Radio)):field.widget.attrs['onchange']=cbsetstateelifisinstance(field.widget,fwdgs.Input):field.widget.attrs['onkeypress']=cbsetstate# XXX elsew(u'<div>%s</div>'%field.render(form,self))w(u'</td>\n')w(u'</tr>')else:self._main_display_fields=fieldsclassEntityFormRenderer(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')defopen_form(self,form,values):attrs_fs_label=''ifself.main_form_title:attrs_fs_label+=('<div class="iformTitle"><span>%s</span></div>'%self._cw._(self.main_form_title))attrs_fs_label+='<div class="formBody">'returnattrs_fs_label+super(EntityFormRenderer,self).open_form(form,values)defclose_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 <form> """returnsuper(EntityFormRenderer,self).close_form(form,values)+'</div>'defrender_buttons(self,w,form):iflen(form.form_buttons)==3:w("""<table width="100%%"> <tbody> <tr><td align="center">%s </td><td style="align: right; width: 50%%;">%s%s </td></tr> </tbody> </table>"""%tuple(button.render(form)forbuttoninform.form_buttons))else:super(EntityFormRenderer,self).render_buttons(w,form)classEntityInlinedFormRenderer(EntityFormRenderer):"""This is a specific renderer for entity's form inlined into another entity's form. """__regid__='inline'defrender(self,w,form,values):form.add_media()try:w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">'%values)exceptKeyError:w(u'<div id="div-%(divid)s">'%values)else:w(u'<div id="notice-%s" class="notice">%s</div>'%(values['divid'],self._cw._('click on the box to cancel the deletion')))w(u'<div class="iformBody">')eschema=form.edited_entity.e_schemaifvalues['removejs']:values['removemsg']=self._cw._('remove-inlined-entity-form')w(u'<div class="iformTitle"><span>%(title)s</span> ''#<span class="icounter">%(counter)s</span> ''[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]</div>'%values)else:w(u'<div class="iformTitle"><span>%(title)s</span> ''#<span class="icounter">%(counter)s</span></div>'%values)# XXX that stinks# cleanup valuesforkeyin('title','removejs','removemsg'):values.pop(key,None)self.render_fields(w,form,values)w(u'</div></div>')defrender_fields(self,w,form,values):w(u'<fieldset id="fs-%(divid)s">'%values)fields=self._render_hidden_fields(w,form)w(u'</fieldset>')w(u'<fieldset class="subentity">')iffields:self._render_fields(fields,w,form)self.render_child_forms(w,form,values)w(u'</fieldset>')