"""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):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,initial=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):ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsfields.remove(field)@iclassmethoddefappend_field(cls_or_self,field):ifisinstance(cls_or_self,type):fields=cls_or_self._fields_else:fields=cls_or_self.fieldsfields.append(field)@propertydefform_needs_multipart(self):returnany(field.needs_multipartforfieldinself.fields)defform_add_hidden(self,name,value=None,**kwargs):field=StringField(name=name,widget=fwdgs.HiddenInput,initial=value,**kwargs)self.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):renderer=values.pop('renderer',FormRenderer())returnrenderer.render(self,values)defform_build_context(self,values=None):self.context=context={}# on validation error, we get a dictionnary of previously submitted valuesifvaluesisNone:values={}previous_values=self.req.data.get('formvalues')ifprevious_values:values.update(previous_values)forfieldinself.fields:forfieldinfield.actual_fields(self):field.form_init(self)value=self.form_field_value(field,values)context[field]={'value':field.format_value(self.req,value),'rawvalue':value,'name':self.form_field_name(field),'id':self.form_field_id(field),}defform_field_value(self,field,values,load_bytes=False):"""looks for field's value in 1. kw args given to render_form (including previously submitted form values if any) 2. req.form 3. field's initial value """iffield.nameinvalues:value=values[field.name]eliffield.nameinself.req.form:value=self.req.form[field.name]else:value=field.initialreturnvaluedefform_field_error(self,field):"""return validation error for widget's field, if any"""errex=self.req.data.get('formerrors')iferrexandfield.nameinerrex.errors:self.req.data['displayederrors'].add(field.name)returnu'<span class="error">%s</span>'%errex.errors[field.name]returnu''defform_field_format(self,field):returnself.req.property_value('ui.default-text-format')defform_field_encoding(self,field):returnself.req.encodingdefform_field_name(self,field):returnfield.namedefform_field_id(self,field):returnfield.iddefform_field_vocabulary(self,field,limit=None):raiseNotImplementedErrorclassEntityFieldsForm(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.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)defform_build_context(self,values=None):self.form_add_entity_hiddens(self.edited_entity.e_schema)returnsuper(EntityFieldsForm,self).form_build_context(values)defform_add_entity_hiddens(self,eschema):forfieldinself.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(self.form_entity_hidden_field(field))defform_entity_hidden_field(self,field):"""returns the hidden field which will indicate the value before the modification """# Only RelationField has a `role` attribute, others are used# to describe attribute fields => role is 'subject'ifgetattr(field,'role','subject')=='subject':name='edits-%s'%field.nameelse:name='edito-%s'%field.namereturnHiddenInitialValueField(field,name=name)defform_field_value(self,field,values,load_bytes=False):"""look for field's value with the following rules: 1. handle special __type and eid fields 2. looks in kw args given to render_form (including previously submitted form values if any) 3. looks in req.form 4. if entity has an eid: 1. looks for an associated attribute / method 2. use field's initial value else: 1. looks for a default_<fieldname> attribute / method on the form 2. use field's initial value values found in step 4 may be a callable which'll then be called. """fieldname=field.nameiffieldname.startswith('edits-')orfieldname.startswith('edito-'):# edit[s|o]- fieds must have the actual value stored on the entityifhasattr(field,'visible_field'):ifself.edited_entity.has_eid():value=self._form_field_entity_value(field.visible_field,default_initial=False)else:value=INTERNAL_FIELD_VALUEelse:value=field.initialeliffieldname=='__type':value=self.edited_entity.ideliffieldname=='eid':value=self.edited_entity.eideliffieldnameinvalues:value=values[fieldname]eliffieldnameinself.req.form:value=self.req.form[fieldname]else:ifself.edited_entity.has_eid()andfield.eidparam:# use value found on the entity or field's initial value if it's# not an attribute of the entity (XXX may conflicts and get# undesired value)value=self._form_field_entity_value(field,default_initial=True,load_bytes=load_bytes)else:defaultattr='default_%s'%fieldnameifhasattr(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)elifhasattr(self,defaultattr):# search for default_<field name> on the form instancevalue=getattr(self,defaultattr)else:# use field's initial valuevalue=field.initialifcallable(value):value=value()returnvaluedefform_field_format(self,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.attribute_metadata(field.name,'format')returnself.req.property_value('ui.default-text-format')defform_field_encoding(self,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.attribute_metadata(field.name,'encoding')returnsuper(EntityFieldsForm,self).form_field_encoding(field)defform_field_error(self,field):"""return validation error for widget's field, if any"""errex=self.req.data.get('formerrors')iferrexanderrex.eid==self.edited_entity.eidandfield.nameinerrex.errors:self.req.data['displayederrors'].add(field.name)returnu'<span class="error">%s</span>'%errex.errors[field.name]returnu''def_form_field_entity_value(self,field,default_initial=True,load_bytes=False):attr=field.nameentity=self.edited_entityiffield.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 attachedreturnTruereturnFalseifdefault_initial:value=getattr(entity,attr,field.initial)else:value=getattr(entity,attr)ifisinstance(field,RelationField):# in this case, value is the list of related entitiesvalue=[ent.eidforentinvalue]returnvaluedefform_field_name(self,field):iffield.eidparam:returneid_param(field.name,self.edited_entity.eid)returnfield.namedefform_field_id(self,field):iffield.eidparam:returneid_param(field.id,self.edited_entity.eid)returnfield.iddefform_field_vocabulary(self,field,limit=None):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:breakreturnresultdefrelation_vocabulary(self,rtype,targettype,role,limit=None,done=None):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))returnresdefsubject_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):subform.is_subform=Trueself.forms.append(subform)