"""The automatic entity form.: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"_=unicodefromlogilab.common.decoratorsimporticlassmethod,cachedfromcubicwebimporttyped_eidfromcubicweb.webimportstdmsgs,uicfgfromcubicweb.webimportform,formwidgetsasfwdgsfromcubicweb.web.formfieldsimportguess_fieldfromcubicweb.web.viewsimportforms,editformsclassAutomaticEntityForm(forms.EntityFieldsForm):"""base automatic form to edit any entity. Designed to be fully generated from schema but highly configurable through: * rtags (rcategories, rfields, rwidgets, inlined, rpermissions) * various standard form parameters XXX s/rtags/uicfg/ ? You can also easily customise it by adding/removing fields in AutomaticEntityForm instances. """id='edition'cwtarget='eformframe'cssclass='entityForm'copy_nav_params=Trueform_buttons=[fwdgs.SubmitButton(),fwdgs.Button(stdmsgs.BUTTON_APPLY,cwaction='apply'),fwdgs.Button(stdmsgs.BUTTON_CANCEL,cwaction='cancel')]attrcategories=('primary','secondary')# class attributes below are actually stored in the uicfg module since we# don't want them to be reloadedrcategories=uicfg.autoform_sectionrfields=uicfg.autoform_fieldrfields_kwargs=uicfg.autoform_field_kwargsrinlined=uicfg.autoform_is_inlinedrpermissions_overrides=uicfg.autoform_permissions_overrides# class methods mapping schema relations to fields in the form ############@classmethoddeferelations_by_category(cls,entity,categories=None,permission=None,rtags=None,strict=False):"""return a list of (relation schema, target schemas, role) matching categories and permission `strict`: bool telling if having local role is enough (strict = False) or not """ifcategoriesisnotNone:ifnotisinstance(categories,(list,tuple,set,frozenset)):categories=(categories,)ifnotisinstance(categories,(set,frozenset)):categories=frozenset(categories)eschema=entity.e_schemaifrtagsisNone:rtags=cls.rcategoriespermsoverrides=cls.rpermissions_overridesifentity.has_eid():eid=entity.eidelse:eid=Nonestrict=Falseforrschema,targetschemas,roleineschema.relation_definitions(True):# check category first, potentially lower cost than checking# permission which may imply rql queriesifcategoriesisnotNone:targetschemas=[tschemafortschemaintargetschemasifrtags.etype_get(eschema,rschema,role,tschema)incategories]ifnottargetschemas:continueifpermissionisnotNone:# tag allowing to hijack the permission machinery when# permission is not verifiable until the entity is actually# created...ifeidisNoneand'%s_on_new'%permissioninpermsoverrides.etype_get(eschema,rschema,role):yield(rschema,targetschemas,role)continueifrschema.final:ifnotrschema.has_perm(entity.req,permission,eid):continueelifrole=='subject':ifnot((notstrictandrschema.has_local_role(permission))orrschema.has_perm(entity.req,permission,fromeid=eid)):continue# on relation with cardinality 1 or ?, we need delete perm as well# if the relation is already setif(permission=='add'andrschema.cardinality(eschema,targetschemas[0],role)in'1?'andeidandentity.related(rschema.type,role)andnotrschema.has_perm(entity.req,'delete',fromeid=eid,toeid=entity.related(rschema.type,role)[0][0])):continueelifrole=='object':ifnot((notstrictandrschema.has_local_role(permission))orrschema.has_perm(entity.req,permission,toeid=eid)):continue# on relation with cardinality 1 or ?, we need delete perm as well# if the relation is already setif(permission=='add'andrschema.cardinality(targetschemas[0],eschema,role)in'1?'andeidandentity.related(rschema.type,role)andnotrschema.has_perm(entity.req,'delete',toeid=eid,fromeid=entity.related(rschema.type,role)[0][0])):continueyield(rschema,targetschemas,role)@classmethoddefesrelations_by_category(cls,entity,categories=None,permission=None,strict=False):"""filter out result of relations_by_category(categories, permission) by removing final relations return a sorted list of (relation's label, relation'schema, role) """result=[]forrschema,ttypes,roleincls.erelations_by_category(entity,categories,permission,strict=strict):ifrschema.final:continueresult.append((rschema.display_name(entity.req,role),rschema,role))returnsorted(result)@iclassmethoddeffield_by_name(cls_or_self,name,role='subject',eschema=None):"""return field with the given name and role. If field is not explicitly defined for the form but `eclass` is specified, guess_field will be called. """try:returnsuper(AutomaticEntityForm,cls_or_self).field_by_name(name,role)exceptform.FieldNotFound:ifeschemaisNoneornotnameincls_or_self.schema:raiserschema=cls_or_self.schema.rschema(name)# XXX use a sample target type. Document this.tschemas=rschema.targets(eschema,role)fieldcls=cls_or_self.rfields.etype_get(eschema,rschema,role,tschemas[0])kwargs=cls_or_self.rfields_kwargs.etype_get(eschema,rschema,role,tschemas[0])ifkwargsisNone:kwargs={}iffieldcls:ifnotisinstance(fieldcls,type):returnfieldcls# already and instancereturnfieldcls(name=name,role=role,eidparam=True,**kwargs)field=guess_field(eschema,rschema,role,eidparam=True,**kwargs)iffieldisNone:raisereturnfield# base automatic entity form methods #######################################def__init__(self,*args,**kwargs):super(AutomaticEntityForm,self).__init__(*args,**kwargs)entity=self.edited_entityifentity.has_eid():entity.complete()forrschema,roleinself.editable_attributes():try:self.field_by_name(rschema.type,role)continue# explicitly specifiedexceptform.FieldNotFound:# has to be guessedtry:field=self.field_by_name(rschema.type,role,eschema=entity.e_schema)self.fields.append(field)exceptform.FieldNotFound:# meta attribute such as <attr>_formatcontinueself.maxrelitems=self.req.property_value('navigation.related-limit')self.force_display=bool(self.req.form.get('__force_display'))@propertydefrelated_limit(self):ifself.force_display:returnNonereturnself.maxrelitems+1@propertydefform_needs_multipart(self):"""true if the form needs enctype=multipart/form-data"""returnself._subform_needs_multipart()defbuild_context(self,rendervalues=None):super(AutomaticEntityForm,self).build_context(rendervalues)forforminself.inlined_forms():form.build_context(rendervalues)def_subform_needs_multipart(self,_tested=None):if_testedisNone:_tested=set()ifsuper(AutomaticEntityForm,self).form_needs_multipart:returnTrue# take a look at inlined forms to check (recursively) if they# need multipart handling.# XXX: this is very suboptimal because inlined forms will be# selected / instantiated twice : here and during form rendering.# Potential solutions:# -> use subforms for inlined forms to get easiser access# -> use a simple onload js function to check if there is# a input type=file in the form# -> generate the <form> node when the content is rendered# and we know the correct enctype (formrenderer's w attribute# is not a StringIO)forformviewinself.inlined_form_views():ifformview.form:ifhasattr(formview.form,'_subform_needs_multipart'):needs_multipart=formview.form._subform_needs_multipart(_tested)else:needs_multipart=formview.form.form_needs_multipartifneeds_multipart:returnTruereturnFalsedefaction(self):"""return the form's action attribute. Default to validateform if not explicitly overriden. """try:returnself._actionexceptAttributeError:returnself.build_url('validateform')defset_action(self,value):"""override default action"""self._action=valueaction=property(action,set_action)# methods mapping edited entity relations to fields in the form ############defrelations_by_category(self,categories=None,permission=None):"""return a list of (relation schema, target schemas, role) matching given category(ies) and permission """returnself.erelations_by_category(self.edited_entity,categories,permission)definlined_relations(self):"""return a list of (relation schema, target schemas, role) matching given category(ies) and permission """# we'll need an initialized varmaker if there are some inlined relationself.initialize_varmaker()returnself.erelations_by_category(self.edited_entity,True,'add',self.rinlined)defsrelations_by_category(self,categories=None,permission=None,strict=False):"""filter out result of relations_by_category(categories, permission) by removing final relations return a sorted list of (relation's label, relation'schema, role) """returnself.esrelations_by_category(self.edited_entity,categories,permission,strict=strict)defeditable_attributes(self):"""return a list of (relation schema, role) to edit for the entity"""return[(rschema,role)forrschema,_,roleinself.relations_by_category(self.attrcategories,'add')ifrschema!='eid']# generic relations modifier ###############################################defrelations_table(self):"""yiels 3-tuples (rtype, target, related_list) where <related_list> itself a list of : - node_id (will be the entity element's DOM id) - appropriate javascript's togglePendingDelete() function call - status 'pendingdelete' or '' - oneline view of related entity """entity=self.edited_entitypending_deletes=self.req.get_pending_deletes(entity.eid)forlabel,rschema,roleinself.srelations_by_category('generic','add',strict=True):relatedrset=entity.related(rschema,role,limit=self.related_limit)ifrschema.has_perm(self.req,'delete'):toggleable_rel_link_func=editforms.toggleable_relation_linkelse:toggleable_rel_link_func=lambdax,y,z:u''related=[]forrowinxrange(relatedrset.rowcount):nodeid=editforms.relation_id(entity.eid,rschema,role,relatedrset[row][0])ifnodeidinpending_deletes:status=u'pendingDelete'label='+'else:status=u''label='x'dellink=toggleable_rel_link_func(entity.eid,nodeid,label)eview=self.view('oneline',relatedrset,row=row)related.append((nodeid,dellink,status,eview))yield(rschema,role,related)defrestore_pending_inserts(self,cell=False):"""used to restore edition page as it was before clicking on 'search for <some entity type>' """eid=self.edited_entity.eidcell=celland"div_insert_"or"tr"pending_inserts=set(self.req.get_pending_inserts(eid))forpendingidinpending_inserts:eidfrom,rtype,eidto=pendingid.split(':')iftyped_eid(eidfrom)==eid:# subjectlabel=display_name(self.req,rtype,'subject')reid=eidtoelse:label=display_name(self.req,rtype,'object')reid=eidfromjscall="javascript: cancelPendingInsert('%s', '%s', null, %s);" \%(pendingid,cell,eid)rset=self.req.eid_rset(reid)eview=self.view('text',rset,row=0)# XXX find a clean way to handle basketsifrset.description[0][0]=='Basket':eview='%s (%s)'%(eview,display_name(self.req,'Basket'))yieldrtype,pendingid,jscall,label,reid,eview# inlined forms support ####################################################@cacheddefinlined_form_views(self):"""compute and return list of inlined form views (hosting the inlined form object) """allformviews=[]entity=self.edited_entityforrschema,ttypes,roleinself.inlined_relations():# show inline forms only if there's one possible target type# for rschemaiflen(ttypes)!=1:self.warning('entity related by the %s relation should have ''inlined form but there is multiple target types, ''dunno what to do',rschema)continuettype=ttypes[0].typeifself.should_inline_relation_form(rschema,ttype,role):formviews=list(self.inline_edition_form_view(rschema,ttype,role))ifrole=='subject':card=rschema.rproperty(entity.e_schema,ttype,'cardinality')[0]else:card=rschema.rproperty(ttype,entity.e_schema,'cardinality')[1]# there is no related entity and we need at least one: we need to# display one explicit inline-creation viewifself.should_display_inline_creation_form(rschema,formviews,card):formviews+=self.inline_creation_form_view(rschema,ttype,role)# we can create more than one related entity, we thus display a link# to add new related entitiesifself.should_display_add_new_relation_link(rschema,formviews,card):addnewlink=self.vreg['views'].select('inline-addnew-link',self.req,etype=ttype,rtype=rschema,role=role,peid=self.edited_entity.eid,pform=self,card=card)formviews.append(addnewlink)allformviews+=formviewsreturnallformviewsdefinlined_forms(self):forformviewinself.inlined_form_views():ifformview.form:# may be None for the addnew_link artefact formyieldformview.formdefshould_inline_relation_form(self,rschema,targettype,role):"""return true if the given relation with entity has role and a targettype target should be inlined """returnTruedefshould_display_inline_creation_form(self,rschema,existant,card):"""return true if a creation form should be inlined by default true if there is no related entity and we need at least one """returnnotexistantandcardin'1+'orself.req.form.has_key('force_%s_display'%rschema)defshould_display_add_new_relation_link(self,rschema,existant,card):"""return true if we should add a link to add a new creation form (through ajax call) by default true if there is no related entity or if the relation has multiple cardinality """returnnotexistantorcardin'+*'defshould_hide_add_new_relation_link(self,rschema,card):"""return true if once an inlined creation form is added, the 'add new' link should be hidden by default true if the relation has single cardinality """returncardin'1?'definline_edition_form_view(self,rschema,ttype,role):"""yield inline form views for already related entities through the given relation """entity=self.edited_entityrelated=entity.has_eid()andentity.related(rschema,role)ifrelated:vvreg=self.vreg['views']# display inline-edition view for all existing related entitiesfori,relentityinenumerate(related.entities()):ifrelentity.has_perm('update'):yieldvvreg.select('inline-edition',self.req,rset=related,row=i,col=0,rtype=rschema,role=role,peid=entity.eid,pform=self)definline_creation_form_view(self,rschema,ttype,role):"""yield inline form views to a newly related (hence created) entity through the given relation """yieldself.vreg['views'].select('inline-creation',self.req,etype=ttype,rtype=rschema,role=role,peid=self.edited_entity.eid,pform=self)defetype_relation_field(etype,rtype,role='subject'):eschema=AutomaticEntityForm.schema.eschema(etype)returnAutomaticEntityForm.field_by_name(rtype,role,eschema)## default form ui configuration ############################################### use primary and not generated for eid since it has to be an hiddenuicfg.autoform_section.tag_attribute(('*','eid'),'primary')uicfg.autoform_section.tag_attribute(('*','description'),'secondary')uicfg.autoform_section.tag_attribute(('*','creation_date'),'metadata')uicfg.autoform_section.tag_attribute(('*','modification_date'),'metadata')uicfg.autoform_section.tag_attribute(('*','cwuri'),'metadata')uicfg.autoform_section.tag_attribute(('*','has_text'),'generated')uicfg.autoform_section.tag_subject_of(('*','in_state','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','owned_by','*'),'metadata')uicfg.autoform_section.tag_subject_of(('*','created_by','*'),'metadata')uicfg.autoform_section.tag_subject_of(('*','is','*'),'generated')uicfg.autoform_section.tag_object_of(('*','is','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','is_instance_of','*'),'generated')uicfg.autoform_section.tag_object_of(('*','is_instance_of','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','identity','*'),'generated')uicfg.autoform_section.tag_object_of(('*','identity','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','require_permission','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','by_transition','*'),'primary')uicfg.autoform_section.tag_object_of(('*','by_transition','*'),'generated')uicfg.autoform_section.tag_object_of(('*','from_state','*'),'generated')uicfg.autoform_section.tag_object_of(('*','to_state','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','wf_info_for','*'),'primary')uicfg.autoform_section.tag_object_of(('*','wf_info_for','*'),'generated')uicfg.autoform_section.tag_subject_of(('*','for_user','*'),'generated')uicfg.autoform_section.tag_object_of(('*','for_user','*'),'generated')uicfg.autoform_section.tag_subject_of(('CWPermission','require_group','*'),'primary')uicfg.autoform_section.tag_attribute(('CWEType','final'),'generated')uicfg.autoform_section.tag_attribute(('CWRType','final'),'generated')uicfg.autoform_section.tag_attribute(('CWUser','firstname'),'secondary')uicfg.autoform_section.tag_attribute(('CWUser','surname'),'secondary')uicfg.autoform_section.tag_attribute(('CWUser','last_login_time'),'metadata')uicfg.autoform_section.tag_subject_of(('CWUser','in_group','*'),'primary')uicfg.autoform_section.tag_object_of(('*','owned_by','CWUser'),'generated')uicfg.autoform_section.tag_object_of(('*','created_by','CWUser'),'generated')uicfg.autoform_section.tag_object_of(('*','bookmarked_by','CWUser'),'metadata')uicfg.autoform_section.tag_attribute(('Bookmark','path'),'primary')uicfg.autoform_section.tag_subject_of(('*','primary_email','*'),'generic')uicfg.autoform_field_kwargs.tag_attribute(('RQLExpression','expression'),{'widget':fwdgs.TextInput})uicfg.autoform_field_kwargs.tag_attribute(('Bookmark','path'),{'widget':fwdgs.TextInput})uicfg.autoform_field_kwargs.tag_subject_of(('TrInfo','wf_info_for','*'),{'widget':fwdgs.HiddenInput})uicfg.autoform_is_inlined.tag_subject_of(('*','use_email','*'),True)uicfg.autoform_is_inlined.tag_subject_of(('CWRelation','relation_type','*'),True)uicfg.autoform_is_inlined.tag_subject_of(('CWRelation','from_entity','*'),True)uicfg.autoform_is_inlined.tag_subject_of(('CWRelation','to_entity','*'),True)