"""abstract form classes for CubicWeb web client:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"fromwarningsimportwarnfromlogilab.common.compatimportanyfromlogilab.common.decoratorsimporticlassmethodfromcubicweb.appobjectimportAppRsetObjectfromcubicweb.selectorsimportyes,non_final_entity,match_kwargs,one_line_rsetfromcubicweb.viewimportNOINDEX,NOFOLLOWfromcubicweb.commonimporttagsfromcubicweb.webimportINTERNAL_FIELD_VALUE,eid_param,stdmsgsfromcubicweb.web.httpcacheimportNoHTTPCacheManagerfromcubicweb.web.controllerimportNAV_FORM_PARAMETERSfromcubicweb.web.formfieldsimport(Field,StringField,RelationField,HiddenInitialValueField)fromcubicweb.web.formrenderersimportFormRendererfromcubicweb.webimportformwidgetsasfwdgsclassFormViewMixIn(object):"""abstract form view mix-in"""category='form'controller='edit'http_cache_manager=NoHTTPCacheManageradd_to_breadcrumbs=Falsedef__init__(self,req,rset,**kwargs):super(FormViewMixIn,self).__init__(req,rset,**kwargs)# get validation session data which may have been previously set.# deleting validation errors here breaks form reloading (errors are# no more available), they have to be deleted by application's publish# method on successful commitformurl=req.url()forminfo=req.get_session_data(formurl)ifforminfo:req.data['formvalues']=forminfo['values']req.data['formerrors']=errex=forminfo['errors']req.data['displayederrors']=set()# if some validation error occured on entity creation, we have to# get the original variable name from its attributed eidforeid=errex.entityforvar,eidinforminfo['eidmap'].items():ifforeid==eid:errex.eid=varbreakelse:errex.eid=foreiddefhtml_headers(self):"""return a list of html headers (eg something to be inserted between <head> and </head> of the returned page by default forms are neither indexed nor followed """return[NOINDEX,NOFOLLOW]deflinkable(self):"""override since forms are usually linked by an action, so we don't want them to be listed by appli.possible_views """returnFalse# XXX should disappear classFormMixIn(object):"""abstract form mix-in XXX: you should inherit from this FIRST (obscure pb with super call) """definitialize_varmaker(self):varmaker=self.req.get_page_data('rql_varmaker')ifvarmakerisNone:varmaker=self.req.varmakerself.req.set_page_data('rql_varmaker',varmaker)self.varmaker=varmaker# XXX deprecated with new form system. Should disappeardomid='entityForm'category='form'controller='edit'http_cache_manager=NoHTTPCacheManageradd_to_breadcrumbs=Falsedef__init__(self,req,rset,**kwargs):super(FormMixIn,self).__init__(req,rset,**kwargs)# get validation session data which may have been previously set.# deleting validation errors here breaks form reloading (errors are# no more available), they have to be deleted by application's publish# method on successful commitformurl=req.url()forminfo=req.get_session_data(formurl)ifforminfo:req.data['formvalues']=forminfo['values']req.data['formerrors']=errex=forminfo['errors']req.data['displayederrors']=set()# if some validation error occured on entity creation, we have to# get the original variable name from its attributed eidforeid=errex.entityforvar,eidinforminfo['eidmap'].items():ifforeid==eid:errex.eid=varbreakelse:errex.eid=foreiddefhtml_headers(self):"""return a list of html headers (eg something to be inserted between <head> and </head> of the returned page by default forms are neither indexed nor followed """return[NOINDEX,NOFOLLOW]deflinkable(self):"""override since forms are usually linked by an action, so we don't want them to be listed by appli.possible_views """returnFalsedefbutton(self,label,klass='validateButton',tabindex=None,**kwargs):iftabindexisNone:tabindex=self.req.next_tabindex()returntags.input(value=label,klass=klass,**kwargs)defaction_button(self,label,onclick=None,__action=None,**kwargs):ifonclickisNone:onclick="postForm('__action_%s', \'%s\', \'%s\')"%(__action,label,self.domid)returnself.button(label,onclick=onclick,**kwargs)defbutton_ok(self,label=None,type='submit',name='defaultsubmit',**kwargs):label=self.req._(labelorstdmsgs.BUTTON_OK).capitalize()returnself.button(label,name=name,type=type,**kwargs)defbutton_apply(self,label=None,type='button',**kwargs):label=self.req._(labelorstdmsgs.BUTTON_APPLY).capitalize()returnself.action_button(label,__action='apply',type=type,**kwargs)defbutton_delete(self,label=None,type='button',**kwargs):label=self.req._(labelorstdmsgs.BUTTON_DELETE).capitalize()returnself.action_button(label,__action='delete',type=type,**kwargs)defbutton_cancel(self,label=None,type='button',**kwargs):label=self.req._(labelorstdmsgs.BUTTON_CANCEL).capitalize()returnself.action_button(label,__action='cancel',type=type,**kwargs)defbutton_reset(self,label=None,type='reset',name='__action_cancel',**kwargs):label=self.req._(labelorstdmsgs.BUTTON_CANCEL).capitalize()returnself.button(label,type=type,**kwargs)defneed_multipart(self,entity,categories=('primary','secondary')):"""return a boolean indicating if form's enctype should be multipart """forrschema,_,xinentity.relations_by_category(categories):ifentity.get_widget(rschema,x).need_multipart:returnTrue# let's find if any of our inlined entities needs multipartforrschema,targettypes,xinentity.relations_by_category('inlineview'):assertlen(targettypes)==1, \"I'm not able to deal with several targets and inlineview"ttype=targettypes[0]inlined_entity=self.vreg.etype_class(ttype)(self.req,None,None)forirschema,_,xininlined_entity.relations_by_category(categories):ifinlined_entity.get_widget(irschema,x).need_multipart:returnTruereturnFalsedeferror_message(self):"""return formatted error message This method should be called once inlined field errors has been consumed """errex=self.req.data.get('formerrors')# get extra errorsiferrexisnotNone:errormsg=self.req._('please correct the following errors:')displayed=self.req.data['displayederrors']errors=sorted((field,err)forfield,errinerrex.errors.items()ifnotfieldindisplayed)iferrors:iflen(errors)>1:templstr='<li>%s</li>\n'else:templstr=' %s\n'forfield,errinerrors:iffieldisNone:errormsg+=templstr%errelse:errormsg+=templstr%'%s: %s'%(self.req._(field),err)iflen(errors)>1:errormsg='<ul>%s</ul>'%errormsgreturnu'<div class="errorMessage">%s</div>'%errormsgreturnu''###############################################################################classmetafieldsform(type):"""metaclass for FieldsForm to retreive fields defined as class attribute and put them into a single ordered list, '_fields_'. """def__new__(mcs,name,bases,classdict):allfields=[]forbaseinbases:ifhasattr(base,'_fields_'):allfields+=base._fields_clsfields=(itemforiteminclassdict.items()ifisinstance(item[1],Field))forfieldname,fieldinsorted(clsfields,key=lambdax:x[1].creation_rank):ifnotfield.name:field.set_name(fieldname)allfields.append(field)classdict['_fields_']=allfieldsreturnsuper(metafieldsform,mcs).__new__(mcs,name,bases,classdict)classFieldNotFound(Exception):"""raised by field_by_name when a field with the given name has not been found """classFieldsForm(FormMixIn,AppRsetObject):__metaclass__=metafieldsform__registry__='forms'__select__=yes()is_subform=False# attributes overrideable through __init__internal_fields=('__errorurl',)+NAV_FORM_PARAMETERSneeds_js=('cubicweb.ajax.js','cubicweb.edition.js',)needs_css=('cubicweb.form.css',)domid='form'title=Noneaction=Noneonsubmit="return freezeFormButtons('%(domid)s');"cssclass=Nonecssstyle=Nonecwtarget=Noneredirect_path=Noneset_error_url=Truecopy_nav_params=Falseform_buttons=None# form buttons (button widgets instances)def__init__(self,req,rset=None,row=None,col=None,submitmsg=None,**kwargs):super(FieldsForm,self).__init__(req,rset,row=row,col=col)forkey,valinkwargs.items():asserthasattr(self.__class__,key)andnotkey[0]=='_',keysetattr(self,key,val)self.fields=list(self.__class__._fields_)ifself.set_error_url:self.form_add_hidden('__errorurl',req.url())ifself.copy_nav_params:forparaminNAV_FORM_PARAMETERS:value=kwargs.get(param,req.form.get(param))ifvalue:self.form_add_hidden(param,value)ifsubmitmsgisnotNone:self.form_add_hidden('__message',submitmsg)self.context=None@iclassmethoddeffield_by_name(cls_or_self,name,role='subject'):"""return field with the given name and role"""ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsforfieldinfields:iffield.name==nameandfield.role==role:returnfieldraiseFieldNotFound(name)@iclassmethoddefremove_field(cls_or_self,field):"""remove a field from form class or instance"""ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsfields.remove(field)@iclassmethoddefappend_field(cls_or_self,field):"""append a field to form class or instance"""ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsfields.append(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"""field=StringField(name=name,widget=fwdgs.HiddenInput,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.fields.append(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',FormRenderer())returnrenderer.render(self,values)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={}# on validation error, we get a dictionary of previously submitted# valuesself._previous_values=self.req.data.get('formvalues',{})# ensure rendervalues is a dictifrendervaluesisNone:rendervalues={}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. """iffield.nameinself._previous_values:value=self._previous_values[field.name]eliffield.nameinself.req.form:value=self.req.form[field.name]else: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)returnvaluedefform_field_value(self,field,load_bytes=False):"""return field's *typed* value"""value=field.initialifcallable(value):value=value(self)returnvaluedefform_field_error(self,field):"""return validation error for widget's field, if any"""errex=self.req.data.get('formerrors')iferrexandself._errex_match_field(errex,field):self.req.data['displayederrors'].add(field.name)returnu'<span class="error">%s</span>'%errex.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_errex_match_field(self,errex,field):"""return true if the field has some error in given validation exception """returnfield.nameinerrex.errorsclassEntityFieldsForm(FieldsForm):__select__=(match_kwargs('entity')|(one_line_rset&non_final_entity()))internal_fields=FieldsForm.internal_fields+('__type','eid')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.row,self.col)self.form_add_hidden('__type',eidparam=True)self.form_add_hidden('eid')ifmsgisnotNone:# If we need to directly attach the new object to another oneforlinktoinself.req.list_form_param('__linkto'):self.form_add_hidden('__linkto',linkto)msg='%s%s'%(msg,self.req._('and linked'))self.form_add_hidden('__message',msg)def_errex_match_field(self,errex,field):"""return true if the field has some error in given validation exception """returnerrex.eid==self.edited_entity.eidandfield.nameinerrex.errorsdef_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_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_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 t o 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.idiffield.role=='object':attr='reverse_'+attrelifentity.e_schema.subject_relation(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():value=getattr(entity,attr)else:value=self._form_field_default_value(field,load_bytes)returnvalue# non final relation fieldifentity.has_eid():value=[ent.eidforentingetattr(entity,attr)]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,rschema,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)classCompositeForm(FieldsForm):"""form composed for sub-forms"""def__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)