"""some base form classes for CubicWeb web client:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromwarningsimportwarnfromlogilab.common.compatimportanyfromlogilab.common.deprecationimportdeprecatedfromcubicweb.selectorsimportnon_final_entity,match_kwargs,one_line_rsetfromcubicweb.webimportINTERNAL_FIELD_VALUE,eid_paramfromcubicweb.webimportform,formwidgetsasfwdgsfromcubicweb.web.controllerimportNAV_FORM_PARAMETERSfromcubicweb.web.formfieldsimportStringFieldclassFieldsForm(form.Form):"""base class for fields based forms. The following attributes may be either set on subclasses or given on form selection to customize the generated form: * `needs_js`: sequence of javascript files that should be added to handle this form (through `req.add_js`) * `needs_css`: sequence of css files that should be added to handle this form (through `req.add_css`) * `domid`: value for the "id" attribute of the <form> tag * `action`: value for the "action" attribute of the <form> tag * `onsubmit`: value for the "onsubmit" attribute of the <form> tag * `cssclass`: value for the "class" attribute of the <form> tag * `cssstyle`: value for the "style" attribute of the <form> tag * `cwtarget`: value for the "cubicweb:target" attribute of the <form> tag * `redirect_path`: relative to redirect to after submitting the form * `copy_nav_params`: flag telling if navigation paramenters should be copied back in hidden input * `form_buttons`: form buttons sequence (button widgets instances) * `form_renderer_id`: id of the form renderer to use to render the form * `fieldsets_in_order`: fieldset name sequence, to control order """__regid__='base'internal_fields=('__errorurl',)+NAV_FORM_PARAMETERS# attributes overrideable by subclasses or through __init__needs_js=('cubicweb.ajax.js','cubicweb.edition.js',)needs_css=('cubicweb.form.css',)domid='form'action=Noneonsubmit="return freezeFormButtons('%(domid)s');"cssclass=Nonecssstyle=Nonecwtarget=Noneredirect_path=Nonecopy_nav_params=Falseform_buttons=Noneform_renderer_id='default'fieldsets_in_order=Nonedef__init__(self,req,rset=None,row=None,col=None,submitmsg=None,mainform=True,**kwargs):super(FieldsForm,self).__init__(req,rset=rset,row=row,col=col)self.fields=list(self.__class__._fields_)forkey,valinkwargs.items():ifkeyinNAV_FORM_PARAMETERS:self.form_add_hidden(key,val)elifhasattr(self.__class__,key)andnotkey[0]=='_':setattr(self,key,val)else:self.cw_extra_kwargs[key]=val# skip other parameters, usually given for selection# (else write a custom class to handle them)ifmainform:self.form_add_hidden('__errorurl',self.session_key())self.form_add_hidden('__domid',self.domid)self.restore_previous_post(self.session_key())# XXX why do we need two different variables (mainform and copy_nav_params ?)ifself.copy_nav_params:forparaminNAV_FORM_PARAMETERS:ifnotparaminkwargs:value=req.form.get(param)ifvalue:self.form_add_hidden(param,value)ifsubmitmsgisnotNone:self.form_add_hidden('__message',submitmsg)self.context=Noneif'domid'inkwargs:# session key changedself.restore_previous_post(self.session_key())@propertydefform_needs_multipart(self):"""true if the form needs enctype=multipart/form-data"""returnany(field.needs_multipartforfieldinself.fields)defform_add_hidden(self,name,value=None,**kwargs):"""add an hidden field to the form"""kwargs.setdefault('widget',fwdgs.HiddenInput)field=StringField(name=name,initial=value,**kwargs)if'id'inkwargs:# by default, hidden input don't set id attribute. If one is# explicitly specified, ensure it will be setfield.widget.setdomid=Trueself.append_field(field)returnfielddefadd_media(self):"""adds media (CSS & JS) required by this widget"""ifself.needs_js:self._cw.add_js(self.needs_js)ifself.needs_css:self._cw.add_css(self.needs_css)defrender(self,formvalues=None,rendervalues=None,renderer=None):"""render this form, using the renderer given in args or the default FormRenderer() """self.build_context(formvaluesor{})ifrendererisNone:renderer=self.form_default_renderer()returnrenderer.render(self,rendervaluesor{})defform_default_renderer(self):returnself._cw.vreg['formrenderers'].select(self.form_renderer_id,self._cw,rset=self.cw_rset,row=self.cw_row,col=self.cw_col)defbuild_context(self,rendervalues=None):"""build form context values (the .context attribute which is a dictionary with field instance as key associated to a dictionary containing field 'name' (qualified), 'id', 'value' (for display, always a string). rendervalues is an optional dictionary containing extra form values given to render() """ifself.contextisnotNone:return# already builtself.context=context={}# ensure rendervalues is a dictifrendervaluesisNone:rendervalues={}# use a copy in case fields are modified while context is build (eg# __linkto handling for instance)forfieldinself.fields[:]:forfieldinfield.actual_fields(self):field.form_init(self)value=self.form_field_display_value(field,rendervalues)context[field]={'value':value,'name':self.form_field_name(field),'id':self.form_field_id(field),}defform_field_display_value(self,field,rendervalues,load_bytes=False):"""return field's *string* value to use for display looks in 1. previously submitted form values if any (eg on validation error) 2. req.form 3. extra kw args given to render_form 4. field's typed value values found in 1. and 2. are expected te be already some 'display' value while those found in 3. and 4. are expected to be correctly typed. """value=self._req_display_value(field)ifvalueisNone:iffield.nameinrendervalues:value=rendervalues[field.name]eliffield.nameinself.cw_extra_kwargs:value=self.cw_extra_kwargs[field.name]else:value=self.form_field_value(field,load_bytes)ifcallable(value):value=value(self)ifvalue!=INTERNAL_FIELD_VALUE:value=field.format_value(self._cw,value)returnvaluedef_req_display_value(self,field):qname=self.form_field_name(field)ifqnameinself.form_previous_values:returnself.form_previous_values[qname]ifqnameinself._cw.form:returnself._cw.form[qname]iffield.nameinself._cw.form:returnself._cw.form[field.name]returnNonedefform_field_value(self,field,load_bytes=False):"""return field's *typed* value"""myattr='%s_%s_default'%(field.role,field.name)ifhasattr(self,myattr):returngetattr(self,myattr)()value=field.initialifcallable(value):value=value(self)returnvaluedefform_field_error(self,field):"""return validation error for widget's field, if any"""ifself._field_has_error(field):self.form_displayed_errors.add(field.name)returnu'<span class="error">%s</span>'%self.form_valerror.errors[field.name]returnu''defform_field_format(self,field):"""return MIME type used for the given (text or bytes) field"""returnself._cw.property_value('ui.default-text-format')defform_field_encoding(self,field):"""return encoding used for the given (text) field"""returnself._cw.encodingdefform_field_name(self,field):"""return qualified name for the given field"""returnfield.namedefform_field_id(self,field):"""return dom id for the given field"""returnfield.iddefform_field_vocabulary(self,field,limit=None):"""return vocabulary for the given field. Should be overriden in specific forms using fields which requires some vocabulary """raiseNotImplementedErrordefform_field_modified(self,field):returnfield.is_visible()def_field_has_error(self,field):"""return true if the field has some error in given validation exception """returnself.form_valerrorandfield.nameinself.form_valerror.errors@deprecated('use .render(formvalues, rendervalues)')defform_render(self,**values):"""render this form, using the renderer given in args or the default FormRenderer() """self.build_context(values)renderer=values.pop('renderer',None)ifrendererisNone:renderer=self.form_default_renderer()returnrenderer.render(self,values)classEntityFieldsForm(FieldsForm):__regid__='base'__select__=(match_kwargs('entity')|(one_line_rset()&non_final_entity()))internal_fields=FieldsForm.internal_fields+('__type','eid','__maineid')domid='entityForm'def__init__(self,*args,**kwargs):self.edited_entity=kwargs.pop('entity',None)msg=kwargs.pop('submitmsg',None)super(EntityFieldsForm,self).__init__(*args,**kwargs)ifself.edited_entityisNone:self.edited_entity=self.cw_rset.complete_entity(self.cw_rowor0,self.cw_color0)self.form_add_hidden('__type',eidparam=True)self.form_add_hidden('eid')ifkwargs.get('mainform',True):# mainform default to true in parentself.form_add_hidden(u'__maineid',self.edited_entity.eid)# If we need to directly attach the new object to another oneifself._cw.list_form_param('__linkto'):forlinktoinself._cw.list_form_param('__linkto'):self.form_add_hidden('__linkto',linkto)ifmsg:msg='%s%s'%(msg,self._cw._('and linked'))else:msg=self._cw._('entity linked')ifmsg:self.form_add_hidden('__message',msg)defsession_key(self):"""return the key that may be used to store / retreive data about a previous post which failed because of a validation error """try:returnself.force_session_keyexceptAttributeError:# XXX if this is a json request, suppose we should redirect to the# entity primary viewifself.req.json_request:return'%s#%s'%(self.edited_entity.absolute_url(),self.domid)return'%s#%s'%(self.req.url(),self.domid)def_field_has_error(self,field):"""return true if the field has some error in given validation exception """returnsuper(EntityFieldsForm,self)._field_has_error(field) \andself.form_valerror.eid==self.edited_entity.eiddef_relation_vocabulary(self,rtype,targettype,role,limit=None,done=None):"""return unrelated entities for a given relation and target entity type for use in vocabulary """ifdoneisNone:done=set()rset=self.edited_entity.unrelated(rtype,targettype,role,limit)res=[]forentityinrset.entities():ifentity.eidindone:continuedone.add(entity.eid)res.append((entity.view('combobox'),entity.eid))returnresdef_req_display_value(self,field):value=super(EntityFieldsForm,self)._req_display_value(field)ifvalueisNone:value=self.edited_entity.linked_to(field.name,field.role)ifvalue:searchedvalues=['%s:%s:%s'%(field.name,eid,field.role)foreidinvalue]# remove associated __linkto hidden fieldsforfieldinself.root_form.fields_by_name('__linkto'):iffield.initialinsearchedvalues:self.root_form.remove_field(field)else:value=Nonereturnvaluedef_form_field_default_value(self,field,load_bytes):defaultattr='default_%s'%field.nameifhasattr(self.edited_entity,defaultattr):# XXX bw compat, default_<field name> on the entitywarn('found %s on %s, should be set on a specific form'%(defaultattr,self.edited_entity.__regid__),DeprecationWarning)value=getattr(self.edited_entity,defaultattr)ifcallable(value):value=value()else:value=super(EntityFieldsForm,self).form_field_value(field,load_bytes)returnvaluedefform_default_renderer(self):returnself._cw.vreg['formrenderers'].select(self.form_renderer_id,self._cw,rset=self.cw_rset,row=self.cw_row,col=self.cw_col,entity=self.edited_entity)defform_field_value(self,field,load_bytes=False):"""return field's *typed* value overriden to deal with * special eid / __type * lookup for values on edited entities """attr=field.nameentity=self.edited_entityifattr=='eid':returnentity.eidifnotfield.eidparam:returnsuper(EntityFieldsForm,self).form_field_value(field,load_bytes)ifattr=='__type':returnentity.__regid__ifself._cw.vreg.schema.rschema(attr).final:attrtype=entity.e_schema.destination(attr)ifattrtype=='Password':returnentity.has_eid()andINTERNAL_FIELD_VALUEor''ifattrtype=='Bytes':ifentity.has_eid():ifload_bytes:returngetattr(entity,attr)# XXX value should reflect if some file is already attachedreturnTruereturnFalseifentity.has_eid()orattrinentity:value=getattr(entity,attr)else:value=self._form_field_default_value(field,load_bytes)returnvalue# non final relation fieldifentity.has_eid()orentity.relation_cached(attr,field.role):value=[r[0]forrinentity.related(attr,field.role)]else:value=self._form_field_default_value(field,load_bytes)returnvaluedefform_field_format(self,field):"""return MIME type used for the given (text or bytes) field"""entity=self.edited_entityiffield.eidparamandentity.e_schema.has_metadata(field.name,'format')and(entity.has_eid()or'%s_format'%field.nameinentity):returnself.edited_entity.attr_metadata(field.name,'format')returnself._cw.property_value('ui.default-text-format')defform_field_encoding(self,field):"""return encoding used for the given (text) field"""entity=self.edited_entityiffield.eidparamandentity.e_schema.has_metadata(field.name,'encoding')and(entity.has_eid()or'%s_encoding'%field.nameinentity):returnself.edited_entity.attr_metadata(field.name,'encoding')returnsuper(EntityFieldsForm,self).form_field_encoding(field)defform_field_name(self,field):"""return qualified name for the given field"""iffield.eidparam:returneid_param(field.name,self.edited_entity.eid)returnfield.namedefform_field_id(self,field):"""return dom id for the given field"""iffield.eidparam:returneid_param(field.id,self.edited_entity.eid)returnfield.id# XXX all this vocabulary handling should be on the field, no?defform_field_vocabulary(self,field,limit=None):"""return vocabulary for the given field"""role,rtype=field.role,field.namemethod='%s_%s_vocabulary'%(role,rtype)try:vocabfunc=getattr(self,method)exceptAttributeError:try:# XXX bw compat, <role>_<rtype>_vocabulary on the entityvocabfunc=getattr(self.edited_entity,method)exceptAttributeError:vocabfunc=getattr(self,'%s_relation_vocabulary'%role)else:warn('found %s on %s, should be set on a specific form'%(method,self.edited_entity.__regid__),DeprecationWarning)# NOTE: it is the responsibility of `vocabfunc` to sort the result# (direclty through RQL or via a python sort). This is also# important because `vocabfunc` might return a list with# couples (label, None) which act as separators. In these# cases, it doesn't make sense to sort results afterwards.returnvocabfunc(rtype,limit)defform_field_modified(self,field):iffield.is_visible():# fields not corresponding to an entity attribute / relations# are considered modifiedifnotfield.eidparamornotself.edited_entity.has_eid():returnTrue# XXXtry:iffield.role=='subject':previous_value=getattr(self.edited_entity,field.name)else:previous_value=getattr(self.edited_entity,'reverse_%s'%field.name)exceptAttributeError:# fields with eidparam=True but not corresponding to an actual# attribute or relationreturnTrue# if it's a non final relation, we need the eidsifisinstance(previous_value,list):# widget should return untyped eidsprevious_value=set(unicode(e.eid)foreinprevious_value)new_value=field.process_form_value(self)ifself.edited_entity.has_eid()and(previous_value==new_value):returnFalse# not modifiedreturnTruereturnFalsedefsubject_relation_vocabulary(self,rtype,limit=None):"""defaut vocabulary method for the given relation, looking for relation's object entities (i.e. self is the subject) """entity=self.edited_entityifisinstance(rtype,basestring):rtype=self._cw.vreg.schema.rschema(rtype)done=Noneassertnotrtype.final,rtypeifentity.has_eid():done=set(e.eidforeingetattr(entity,str(rtype)))result=[]rsetsize=Noneforobjtypeinrtype.objects(entity.e_schema):iflimitisnotNone:rsetsize=limit-len(result)result+=self._relation_vocabulary(rtype,objtype,'subject',rsetsize,done)iflimitisnotNoneandlen(result)>=limit:breakreturnresultdefobject_relation_vocabulary(self,rtype,limit=None):"""defaut vocabulary method for the given relation, looking for relation's subject entities (i.e. self is the object) """entity=self.edited_entityifisinstance(rtype,basestring):rtype=self._cw.vreg.schema.rschema(rtype)done=Noneifentity.has_eid():done=set(e.eidforeingetattr(entity,'reverse_%s'%rtype))result=[]rsetsize=Noneforsubjtypeinrtype.subjects(entity.e_schema):iflimitisnotNone:rsetsize=limit-len(result)result+=self._relation_vocabulary(rtype,subjtype,'object',rsetsize,done)iflimitisnotNoneandlen(result)>=limit:breakreturnresultdefeditable_relations(self):return()defshould_display_add_new_relation_link(self,rschema,existant,card):returnFalseclassCompositeFormMixIn(object):"""form composed of sub-forms"""__regid__='composite'form_renderer_id=__regid__def__init__(self,*args,**kwargs):super(CompositeFormMixIn,self).__init__(*args,**kwargs)self.forms=[]defadd_subform(self,subform):"""mark given form as a subform and append it"""subform.parent_form=selfself.forms.append(subform)defbuild_context(self,rendervalues=None):super(CompositeFormMixIn,self).build_context(rendervalues)forforminself.forms:form.build_context(rendervalues)classCompositeForm(CompositeFormMixIn,FieldsForm):passclassCompositeEntityForm(CompositeFormMixIn,EntityFieldsForm):pass# XXX why is this class necessary?