"""The edit controller, handling form submitting.: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"fromdecimalimportDecimalfromrql.utilsimportrqlvar_makerfromcubicwebimportBinary,ValidationError,typed_eidfromcubicweb.webimportINTERNAL_FIELD_VALUE,RequestError,NothingToEditfromcubicweb.web.controllerimportparse_relations_descrfromcubicweb.web.views.basecontrollersimportViewControllerclassToDoLater(Exception):"""exception used in the edit controller to indicate that a relation can't be handled right now and have to be handled later """classEditController(ViewController):id='edit'defpublish(self,rset=None):"""edit / create / copy / delete entity / relations"""forkeyinself.req.form:# There should be 0 or 1 actionifkey.startswith('__action_'):cbname=key[1:]try:callback=getattr(self,cbname)exceptAttributeError:raiseRequestError(self.req._('invalid action %r'%key))else:returncallback()self._default_publish()self.reset()def_default_publish(self):req=self.reqform=req.form# no specific action, generic editionself._to_create=req.data['eidmap']={}self._pending_relations=[]todelete=self.req.get_pending_deletes()toinsert=self.req.get_pending_inserts()try:methodname=form.pop('__method',None)foreidinreq.edited_eids():# __type and eidformparams=req.extract_entity_params(eid,minparams=2)ifmethodnameisnotNone:entity=req.entity_from_eid(eid)method=getattr(entity,methodname)method(formparams)eid=self.edit_entity(formparams)except(RequestError,NothingToEdit):if'__linkto'informand'eid'inform:self.execute_linkto()elifnot('__delete'informor'__insert'informortodeleteortoinsert):raiseValidationError(None,{None:req._('nothing to edit')})# handle relations in newly created entitiesifself._pending_relations:forrschema,formparams,x,entityinself._pending_relations:self.handle_relation(rschema,formparams,x,entity,True)# XXX this processes *all* pending operations of *all* entitiesifform.has_key('__delete'):todelete+=req.list_form_param('__delete',form,pop=True)iftodelete:self.delete_relations(parse_relations_descr(todelete))ifform.has_key('__insert'):toinsert=req.list_form_param('__insert',form,pop=True)iftoinsert:self.insert_relations(parse_relations_descr(toinsert))self.req.remove_pending_operations()defedit_entity(self,formparams,multiple=False):"""edit / create / copy an entity and return its eid"""etype=formparams['__type']entity=self.vreg['etypes'].etype_class(etype)(self.req)entity.eid=eid=self._get_eid(formparams['eid'])edited=self.req.form.get('__maineid')==formparams['eid']# let a chance to do some entity specific stuff.entity.pre_web_edit()# create a rql query from parametersself.relations=[]self.restrictions=[]# process inlined relations at the same time as attributes# this is required by some external source such as the svn source which# needs some information provided by those inlined relation. Moreover# this will generate less write queries.forrschemainentity.e_schema.subject_relations():ifrschema.final:self.handle_attribute(entity,rschema,formparams)elifrschema.inlined:self.handle_inlined_relation(rschema,formparams,entity)execute=self.req.executeifeidisNone:# creation or copyifself.relations:rql='INSERT %s X: %s'%(etype,','.join(self.relations))else:rql='INSERT %s X'%etypeifself.restrictions:rql+=' WHERE %s'%','.join(self.restrictions)try:# get the new entity (in some cases, the type might have# changed as for the File --> Image mutation)entity=execute(rql,formparams).get_entity(0,0)eid=entity.eidexceptValidationError,ex:self._to_create[formparams['eid']]=ex.entityifself.req.json_request:# XXX (syt) why?ex.entity=formparams['eid']raiseself._to_create[formparams['eid']]=eidelifself.relations:# edition of an existant entityvarmaker=rqlvar_maker()var=varmaker.next()whilevarinformparams:var=varmaker.next()rql='SET %s WHERE X eid %%(%s)s'%(','.join(self.relations),var)ifself.restrictions:rql+=', %s'%','.join(self.restrictions)formparams[var]=eidexecute(rql,formparams)forrschemainentity.e_schema.subject_relations():ifrschema.finalorrschema.inlined:continueself.handle_relation(rschema,formparams,'subject',entity)forrschemainentity.e_schema.object_relations():ifrschema.final:continueself.handle_relation(rschema,formparams,'object',entity)ifedited:self.notify_edited(entity)ifformparams.has_key('__delete'):todelete=self.req.list_form_param('__delete',formparams,pop=True)self.delete_relations(parse_relations_descr(todelete))ifformparams.has_key('__cloned_eid'):entity.copy_relations(typed_eid(formparams['__cloned_eid']))ifformparams.has_key('__insert'):toinsert=self.req.list_form_param('__insert',formparams,pop=True)self.insert_relations(parse_relations_descr(toinsert))ifedited:# only execute linkto for the main entityself.execute_linkto(eid)returneiddef_action_apply(self):self._default_publish()self.reset()def_action_cancel(self):errorurl=self.req.form.get('__errorurl')iferrorurl:self.req.cancel_edition(errorurl)self.req.message=self.req._('edit canceled')returnself.reset()def_action_delete(self):self.delete_entities(self.req.edited_eids(withtype=True))returnself.reset()def_needs_edition(self,rtype,formparams,entity):"""returns True and and the new value if `rtype` was edited"""editkey='edits-%s'%rtypeifnoteditkeyinformparams:returnFalse,None# not editedvalue=formparams.get(rtype)orNoneifentity.has_eid()and(formparams.get(editkey)orNone)==value:returnFalse,None# not modifiedifvalue==INTERNAL_FIELD_VALUE:value=NonereturnTrue,valuedefhandle_attribute(self,entity,rschema,formparams):"""append to `relations` part of the rql query to edit the attribute described by the given schema if necessary """attr=rschema.typeedition_needed,value=self._needs_edition(attr,formparams,entity)ifnotedition_needed:return# test if entity class defines a special handler for this attributecustom_edit=getattr(entity,'custom_%s_edit'%attr,None)ifcustom_edit:custom_edit(formparams,value,self.relations)returnattrtype=rschema.objects(entity.e_schema)[0].type# on checkbox or selection, the field may not be in params# NOTE: raising ValidationError here is not a good solution because# we can't gather all errors at once. Hopefully, the new 3.6.x# form handling will fix thatifattrtype=='Boolean':value=bool(value)elifattrtype=='Decimal':value=Decimal(value)elifattrtype=='Bytes':# if it is a file, transport it using a Binary (StringIO)# XXX later __detach is for the new widget system, the former is to# be removed once web/widgets.py has been droppedifformparams.has_key('__%s_detach'%attr)orformparams.has_key('%s__detach'%attr):# drop current file valuevalue=None# no need to check value when nor explicit detach nor new file# submitted, since it will think the attribute is not modifiedelifisinstance(value,unicode):# file modified using a text widgetencoding=entity.attr_metadata(attr,'encoding')value=Binary(value.encode(encoding))elifvalue:# value is a 3-uple (filename, mimetype, stream)val=Binary(value[2].read())ifnotval.getvalue():# usually an unexistant filevalue=Noneelse:val.filename=value[0]# ignore browser submitted MIME type since it may be buggy# XXX add a config option to tell if we should consider it# or not?#if entity.e_schema.has_metadata(attr, 'format'):# key = '%s_format' % attr# formparams[key] = value[1]# self.relations.append('X %s_format %%(%s)s'# % (attr, key))# XXX suppose a File compatible schemaif'name'inentity.e_schema.subjrels \andnotformparams.get('name'):formparams['name']=value[0]self.relations.append('X name %(name)s')value=valelse:# no specified value, skipreturnelifvalueisnotNone:ifattrtype=='Int':try:value=int(value)exceptValueError:raiseValidationError(entity.eid,{attr:self.req._("invalid integer value")})elifattrtype=='Float':try:value=float(value)exceptValueError:raiseValidationError(entity.eid,{attr:self.req._("invalid float value")})elifattrtypein('Date','Datetime','Time'):try:value=self.parse_datetime(value,attrtype)exceptValueError:raiseValidationError(entity.eid,{attr:self.req._("invalid date")})elifattrtype=='Password':# check confirmation (see PasswordWidget for confirmation field name)confirmval=formparams.get(attr+'-confirm')ifconfirmval!=value:raiseValidationError(entity.eid,{attr:self.req._("password and confirmation don't match")})# password should *always* be utf8 encodedvalue=value.encode('UTF8')else:# strip stringsvalue=value.strip()elifattrtype=='Password':# skip None passwordreturn# unset passwordformparams[attr]=valueself.relations.append('X %s%%(%s)s'%(attr,attr))def_relation_values(self,rschema,formparams,x,entity,late=False):"""handle edition for the (rschema, x) relation of the given entity """rtype=rschema.typeeditkey='edit%s-%s'%(x[0],rtype)ifnoteditkeyinformparams:return# not editedtry:values=self._linked_eids(self.req.list_form_param(rtype,formparams),late)exceptToDoLater:self._pending_relations.append((rschema,formparams,x,entity))returnorigvalues=set(typed_eid(eid)foreidinself.req.list_form_param(editkey,formparams))returnvalues,origvaluesdefhandle_inlined_relation(self,rschema,formparams,entity,late=False):"""handle edition for the (rschema, x) relation of the given entity """try:values,origvalues=self._relation_values(rschema,formparams,'subject',entity,late)exceptTypeError:return# not edited / to do laterifvalues==origvalues:return# not modifiedattr=str(rschema)ifvalues:formparams[attr]=iter(values).next()self.relations.append('X %s%s'%(attr,attr.upper()))self.restrictions.append('%s eid %%(%s)s'%(attr.upper(),attr))elifentity.has_eid():self.handle_relation(rschema,formparams,'subject',entity,late)defhandle_relation(self,rschema,formparams,x,entity,late=False):"""handle edition for the (rschema, x) relation of the given entity """try:values,origvalues=self._relation_values(rschema,formparams,x,entity,late)exceptTypeError:return# not edited / to do lateretype=entity.e_schemaifvalues==origvalues:return# not modifiedifx=='subject':desttype=rschema.objects(etype)[0]card=rschema.rproperty(etype,desttype,'cardinality')[0]subjvar,objvar='X','Y'else:desttype=rschema.subjects(etype)[0]card=rschema.rproperty(desttype,etype,'cardinality')[1]subjvar,objvar='Y','X'eid=entity.eidifx=='object'ornotrschema.inlinedornotvalues:# this is not an inlined relation or no values specified,# explicty remove relationsrql='DELETE %s%s%s WHERE X eid %%(x)s, Y eid %%(y)s'%(subjvar,rschema,objvar)forreidinorigvalues.difference(values):self.req.execute(rql,{'x':eid,'y':reid},('x','y'))seteids=values.difference(origvalues)ifseteids:rql='SET %s%s%s WHERE X eid %%(x)s, Y eid %%(y)s'%(subjvar,rschema,objvar)forreidinseteids:self.req.execute(rql,{'x':eid,'y':reid},('x','y'))def_get_eid(self,eid):# should be either an int (existant entity) or a variable (to be# created entity)asserteidoreid==0,repr(eid)# 0 is a valid eidtry:returntyped_eid(eid)exceptValueError:try:returnself._to_create[eid]exceptKeyError:self._to_create[eid]=NonereturnNonedef_linked_eids(self,eids,late=False):"""return a list of eids if they are all known, else raise ToDoLater """result=set()foreidineids:ifnoteid:# AutoCompletionWidgetcontinueeid=self._get_eid(eid)ifeidisNone:ifnotlate:raiseToDoLater()# eid is still None while it's already a late call# this mean that the associated entity has not been createdraiseException('duh')result.add(eid)returnresult