"""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.decoratorsimporticlassmethodfromcubicweb.selectorsimportnon_final_entity,match_kwargs,one_line_rsetfromcubicweb.webimportINTERNAL_FIELD_VALUE,eid_paramfromcubicweb.webimportform,formwidgetsasfwdgsfromcubicweb.web.controllerimportNAV_FORM_PARAMETERSfromcubicweb.web.formfieldsimportHiddenInitialValueField,StringFieldclassFieldsForm(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 """id='base'is_subform=Falseinternal_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,row=row,col=col)self.fields=list(self.__class__._fields_)forkey,valinkwargs.items():ifkeyinNAV_FORM_PARAMETERS:self.form_add_hidden(key,val)else:asserthasattr(self.__class__,key)andnotkey[0]=='_',keysetattr(self,key,val)ifmainform:self.form_add_hidden('__errorurl',self.session_key())self.form_add_hidden('__domid',self.domid)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())@iclassmethoddef_fieldsattr(cls_or_self):ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsreturnfields@iclassmethoddeffield_by_name(cls_or_self,name,role='subject'):"""return field with the given name and role. Raise FieldNotFound if the field can't be found. """forfieldincls_or_self._fieldsattr():iffield.name==nameandfield.role==role:returnfieldraiseform.FieldNotFound(name)@iclassmethoddeffields_by_name(cls_or_self,name,role='subject'):"""return a list of fields with the given name and role"""return[fieldforfieldincls_or_self._fieldsattr()iffield.name==nameandfield.role==role]@iclassmethoddefremove_field(cls_or_self,field):"""remove a field from form class or instance"""cls_or_self._fieldsattr().remove(field)@iclassmethoddefappend_field(cls_or_self,field):"""append a field to form class or instance"""cls_or_self._fieldsattr().append(field)@iclassmethoddefinsert_field_before(cls_or_self,new_field,name,role='subject'):field=cls_or_self.field_by_name(name,role)fields=cls_or_self._fieldsattr()fields.insert(fields.index(field),new_field)@iclassmethoddefinsert_field_after(cls_or_self,new_field,name,role='subject'):field=cls_or_self.field_by_name(name,role)fields=cls_or_self._fieldsattr()fields.insert(fields.index(field)+1,new_field)@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.req.add_js(self.needs_js)ifself.needs_css:self.req.add_css(self.needs_css)defform_render(self,**values):"""render this form, using the renderer given in args or the default FormRenderer() """renderer=values.pop('renderer',None)ifrendererisNone:renderer=self.form_default_renderer()returnrenderer.render(self,values)defform_default_renderer(self):returnself.vreg['formrenderers'].select(self.form_renderer_id,self.req,rset=self.rset,row=self.row,col=self.col)defform_build_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 kwargs given to form_render() """self.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]else:value=self.form_field_value(field,load_bytes)ifcallable(value):value=value(self)ifvalue!=INTERNAL_FIELD_VALUE:value=field.format_value(self.req,value)returnvaluedef_req_display_value(self,field):qname=self.form_field_name(field)ifqnameinself.form_previous_values:returnself.form_previous_values[qname]ifqnameinself.req.form:returnself.req.form[qname]iffield.nameinself.req.form:returnself.req.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.req.property_value('ui.default-text-format')defform_field_encoding(self,field):"""return encoding used for the given (text) field"""returnself.req.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 """raiseNotImplementedErrordef_field_has_error(self,field):"""return true if the field has some error in given validation exception """returnself.form_valerrorandfield.nameinself.form_valerror.errorsclassEntityFieldsForm(FieldsForm):id='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.complete_entity(self.rowor0,self.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.req.list_form_param('__linkto'):forlinktoinself.req.list_form_param('__linkto'):self.form_add_hidden('__linkto',linkto)ifmsg:msg='%s%s'%(msg,self.req._('and linked'))else:msg=self.req._('entity linked')ifmsg:self.form_add_hidden('__message',msg)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.fields_by_name('__linkto'):iffield.initialinsearchedvalues:self.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.id),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.vreg['formrenderers'].select(self.form_renderer_id,self.req,rset=self.rset,row=self.row,col=self.col,entity=self.edited_entity)defform_build_context(self,values=None):"""overriden to add edit[s|o] hidden fields and to ensure schema fields have eidparam set to True edit[s|o] hidden fields are used to indicate the value for the associated field before the (potential) modification made when submitting the form. """eschema=self.edited_entity.e_schemaforfieldinself.fields[:]:forfieldinfield.actual_fields(self):fieldname=field.nameiffieldname!='eid'and((eschema.has_subject_relation(fieldname)oreschema.has_object_relation(fieldname))):field.eidparam=Trueself.fields.append(HiddenInitialValueField(field))returnsuper(EntityFieldsForm,self).form_build_context(values)defform_field_value(self,field,load_bytes=False):"""return field's *typed* value overriden to deal with * special eid / __type / edits- / edito- fields * 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.startswith('edits-')orattr.startswith('edito-'):# edit[s|o]- fieds must have the actual value stored on the entityasserthasattr(field,'visible_field')vfield=field.visible_fieldassertvfield.eidparamifentity.has_eid():returnself.form_field_value(vfield)returnINTERNAL_FIELD_VALUEifattr=='__type':returnentity.idifself.schema.rschema(attr).is_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.req.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.iddefform_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.id),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)defsubject_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=entity.schema.rschema(rtype)done=Noneassertnotrtype.is_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=entity.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:breakreturnresultdefsubject_in_state_vocabulary(self,rtype,limit=None):"""vocabulary method for the in_state relation, looking for relation's object entities (i.e. self is the subject) according to initial_state, state_of and next_state relation """entity=self.edited_entityifnotentity.has_eid()ornotentity.in_state:# get the initial staterql='Any S where S state_of ET, ET name %(etype)s, ET initial_state S'rset=self.req.execute(rql,{'etype':str(entity.e_schema)})ifrset:return[(rset.get_entity(0,0).view('combobox'),rset[0][0])]return[]results=[]fortrinentity.in_state[0].transitions(entity):state=tr.destination_state[0]results.append((state.view('combobox'),state.eid))returnsorted(results)defsrelations_by_category(self,categories=None,permission=None):return()defshould_display_add_new_relation_link(self,rschema,existant,card):returnFalseclassCompositeForm(FieldsForm):"""form composed for sub-forms"""id='composite'form_renderer_id=iddef__init__(self,*args,**kwargs):super(CompositeForm,self).__init__(*args,**kwargs)self.forms=[]defform_add_subform(self,subform):"""mark given form as a subform and append it"""subform.is_subform=Trueself.forms.append(subform)