# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""The edit controller, automatically handling entity form submitting"""__docformat__="restructuredtext en"fromwarningsimportwarnfromcollectionsimportdefaultdictfromdatetimeimportdatetimefromlogilab.common.deprecationimportdeprecatedfromlogilab.common.graphimportordered_nodesfromrql.utilsimportrqlvar_makerfromcubicwebimportBinary,ValidationErrorfromcubicweb.viewimportEntityAdapterfromcubicweb.predicatesimportis_instancefromcubicweb.webimport(INTERNAL_FIELD_VALUE,RequestError,NothingToEdit,ProcessFormError)fromcubicweb.web.viewsimportbasecontrollers,autoformclassIEditControlAdapter(EntityAdapter):__regid__='IEditControl'__select__=is_instance('Any')def__init__(self,_cw,**kwargs):ifself.__class__isnotIEditControlAdapter:warn('[3.14] IEditControlAdapter is deprecated, override EditController'' using match_edited_type or match_form_id selectors for example.',DeprecationWarning)super(IEditControlAdapter,self).__init__(_cw,**kwargs)defafter_deletion_path(self):"""return (path, parameters) which should be used as redirect information when this entity is being deleted """parent=self.entity.cw_adapt_to('IBreadCrumbs').parent_entity()ifparentisnotNone:returnparent.rest_path(),{}returnstr(self.entity.e_schema).lower(),{}defpre_web_edit(self):"""callback called by the web editcontroller when an entity will be created/modified, to let a chance to do some entity specific stuff. Do nothing by default. """passdefvalerror_eid(eid):try:returnint(eid)except(ValueError,TypeError):returneidclassRqlQuery(object):def__init__(self):self.edited=[]self.restrictions=[]self.kwargs={}def__repr__(self):return('Query <edited=%r restrictions=%r kwargs=%r>'%(self.edited,self.restrictions,self.kwargs))definsert_query(self,etype):ifself.edited:rql='INSERT %s X: %s'%(etype,','.join(self.edited))else:rql='INSERT %s X'%etypeifself.restrictions:rql+=' WHERE %s'%','.join(self.restrictions)returnrqldefupdate_query(self,eid):varmaker=rqlvar_maker()var=varmaker.next()whilevarinself.kwargs:var=varmaker.next()rql='SET %s WHERE X eid %%(%s)s'%(','.join(self.edited),var)ifself.restrictions:rql+=', %s'%','.join(self.restrictions)self.kwargs[var]=eidreturnrqldefset_attribute(self,attr,value):self.kwargs[attr]=valueself.edited.append('X %s%%(%s)s'%(attr,attr))defset_inlined(self,relation,value):self.kwargs[relation]=valueself.edited.append('X %s%s'%(relation,relation.upper()))self.restrictions.append('%s eid %%(%s)s'%(relation.upper(),relation))classEditController(basecontrollers.ViewController):__regid__='edit'defpublish(self,rset=None):"""edit / create / copy / delete entity / relations"""forkeyinself._cw.form:# There should be 0 or 1 actionifkey.startswith('__action_'):cbname=key[1:]try:callback=getattr(self,cbname)exceptAttributeError:raiseRequestError(self._cw._('invalid action %r'%key))else:returncallback()self._default_publish()self.reset()def_ordered_formparams(self):""" Return form parameters dictionaries for each edited entity. We ensure that entities can be created in this order accounting for mandatory inlined relations. """req=self._cwgraph={}get_rschema=self._cw.vreg.schema.rschema# minparams = 2, because at least __type and eid are neededvalues_by_eid=dict((eid,req.extract_entity_params(eid,minparams=2))foreidinreq.edited_eids())# iterate over all the edited entitiesforeid,valuesinvalues_by_eid.iteritems():# add eid to the dependency graphgraph.setdefault(eid,set())# search entity's edited fields for mandatory inlined relationforparaminvalues['_cw_entity_fields'].split(','):try:rtype,role=param.split('-')exceptValueError:# e.g. param='__type'continuerschema=get_rschema(rtype)ifrschema.inlined:fortargetinrschema.targets(values['__type'],role):rdef=rschema.role_rdef(values['__type'],target,role)# if cardinality is 1 and if the target entity is being# simultaneously edited, the current entity must be# created before the target oneifrdef.cardinality[0ifrole=='subject'else1]=='1':# use .get since param may be unspecified (though it will usually lead# to a validation error later)target_eid=values.get(param)iftarget_eidinvalues_by_eid:# add dependency from the target entity to the# current oneifrole=='object':graph.setdefault(target_eid,set()).add(eid)else:graph.setdefault(eid,set()).add(target_eid)breakforeidinreversed(ordered_nodes(graph)):yieldvalues_by_eid[eid]def_default_publish(self):req=self._cwself.errors=[]self.relations_rql=[]form=req.form# so we're able to know the main entity from the repository sideif'__maineid'inform:req.transaction_data['__maineid']=form['__maineid']# no specific action, generic editionself._to_create=req.data['eidmap']={}# those two data variables are used to handle relation from/to entities# which doesn't exist at time where the entity is edited and that# deserves special treatmentreq.data['pending_inlined']=defaultdict(set)req.data['pending_others']=set()try:forformparamsinself._ordered_formparams():eid=self.edit_entity(formparams)except(RequestError,NothingToEdit)asex:if'__linkto'inreq.formand'eid'inreq.form:self.execute_linkto()elifnot('__delete'inreq.formor'__insert'inreq.form):raiseValidationError(None,{None:unicode(ex)})# all pending inlined relations to newly created entities have been# treated now (pop to ensure there are no attempt to add new ones)pending_inlined=req.data.pop('pending_inlined')assertnotpending_inlined,pending_inlined# handle all other remaining relations nowforform_,fieldinreq.data.pop('pending_others'):self.handle_formfield(form_,field)# then execute rql to set all relationsforquerydefinself.relations_rql:self._cw.execute(*querydef)# XXX this processes *all* pending operations of *all* entitiesif'__delete'inreq.form:todelete=req.list_form_param('__delete',req.form,pop=True)iftodelete:autoform.delete_relations(self._cw,todelete)self._cw.remove_pending_operations()ifself.errors:errors=dict((f.name,unicode(ex))forf,exinself.errors)raiseValidationError(valerror_eid(form.get('__maineid')),errors)def_insert_entity(self,etype,eid,rqlquery):rql=rqlquery.insert_query(etype)try:entity=self._cw.execute(rql,rqlquery.kwargs).get_entity(0,0)neweid=entity.eidexceptValidationErrorasex:self._to_create[eid]=ex.entityifself._cw.ajax_request:# XXX (syt) why?ex.entity=eidraiseself._to_create[eid]=neweidreturnneweiddef_update_entity(self,eid,rqlquery):self._cw.execute(rqlquery.update_query(eid),rqlquery.kwargs)defedit_entity(self,formparams,multiple=False):"""edit / create / copy an entity and return its eid"""req=self._cwetype=formparams['__type']entity=req.vreg['etypes'].etype_class(etype)(req)entity.eid=valerror_eid(formparams['eid'])is_main_entity=req.form.get('__maineid')==formparams['eid']# let a chance to do some entity specific stuffentity.cw_adapt_to('IEditControl').pre_web_edit()# create a rql query from parametersrqlquery=RqlQuery()# process inlined relations at the same time as attributes# this will generate less rql queries and might be useful in# a few dark cornersifis_main_entity:formid=req.form.get('__form_id','edition')else:# XXX inlined forms formid should be saved in a different formparams entry# inbetween, use cubicweb standard formid for inlined formsformid='edition'form=req.vreg['forms'].select(formid,req,entity=entity)eid=form.actual_eid(entity.eid)editedfields=formparams['_cw_entity_fields']form.formvalues={}# init fields value cacheforfieldinform.iter_modified_fields(editedfields,entity):self.handle_formfield(form,field,rqlquery)# if there are some inlined field which were waiting for this entity's# creation, add relevant data to the rqlqueryforform_,fieldinreq.data['pending_inlined'].pop(entity.eid,()):rqlquery.set_inlined(field.name,form_.edited_entity.eid)ifself.errors:errors=dict((f.role_name(),unicode(ex))forf,exinself.errors)raiseValidationError(valerror_eid(entity.eid),errors)ifeidisNone:# creation or copyentity.eid=eid=self._insert_entity(etype,formparams['eid'],rqlquery)elifrqlquery.edited:# edition of an existant entityself.check_concurrent_edition(formparams,eid)self._update_entity(eid,rqlquery)ifis_main_entity:self.notify_edited(entity)if'__delete'informparams:# XXX deprecate?todelete=req.list_form_param('__delete',formparams,pop=True)autoform.delete_relations(req,todelete)if'__cloned_eid'informparams:entity.copy_relations(int(formparams['__cloned_eid']))ifis_main_entity:# only execute linkto for the main entityself.execute_linkto(entity.eid)returneiddefhandle_formfield(self,form,field,rqlquery=None):eschema=form.edited_entity.e_schematry:forfield,valueinfield.process_posted(form):ifnot((field.role=='subject'andfield.nameineschema.subjrels)or(field.role=='object'andfield.nameineschema.objrels)):continuerschema=self._cw.vreg.schema.rschema(field.name)ifrschema.final:rqlquery.set_attribute(field.name,value)else:ifform.edited_entity.has_eid():origvalues=set(entity.eidforentityinform.edited_entity.related(field.name,field.role,entities=True))else:origvalues=set()ifvalueisNoneorvalue==origvalues:continue# not edited / not modified / to do laterifrschema.inlinedandrqlqueryisnotNoneandfield.role=='subject':self.handle_inlined_relation(form,field,value,origvalues,rqlquery)elifform.edited_entity.has_eid():self.handle_relation(form,field,value,origvalues)else:form._cw.data['pending_others'].add((form,field))exceptProcessFormErrorasexc:self.errors.append((field,exc))defhandle_inlined_relation(self,form,field,values,origvalues,rqlquery):"""handle edition for the (rschema, x) relation of the given entity """ifvalues:rqlquery.set_inlined(field.name,iter(values).next())elifform.edited_entity.has_eid():self.handle_relation(form,field,values,origvalues)defhandle_relation(self,form,field,values,origvalues):"""handle edition for the (rschema, x) relation of the given entity """etype=form.edited_entity.e_schemarschema=self._cw.vreg.schema.rschema(field.name)iffield.role=='subject':desttype=rschema.objects(etype)[0]card=rschema.rdef(etype,desttype).cardinality[0]subjvar,objvar='X','Y'else:desttype=rschema.subjects(etype)[0]card=rschema.rdef(desttype,etype).cardinality[1]subjvar,objvar='Y','X'eid=form.edited_entity.eidiffield.role=='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.relations_rql.append((rql,{'x':eid,'y':reid}))seteids=values.difference(origvalues)ifseteids:rql='SET %s%s%s WHERE X eid %%(x)s, Y eid %%(y)s'%(subjvar,rschema,objvar)forreidinseteids:self.relations_rql.append((rql,{'x':eid,'y':reid}))defdelete_entities(self,eidtypes):"""delete entities from the repository"""redirect_info=set()eidtypes=tuple(eidtypes)foreid,etypeineidtypes:entity=self._cw.entity_from_eid(eid,etype)path,params=entity.cw_adapt_to('IEditControl').after_deletion_path()redirect_info.add((path,tuple(params.iteritems())))entity.cw_delete()iflen(redirect_info)>1:# In the face of ambiguity, refuse the temptation to guess.self._after_deletion_path='view',()else:self._after_deletion_path=iter(redirect_info).next()iflen(eidtypes)>1:self._cw.set_message(self._cw._('entities deleted'))else:self._cw.set_message(self._cw._('entity deleted'))defcheck_concurrent_edition(self,formparams,eid):req=self._cwtry:form_ts=datetime.fromtimestamp(float(formparams['__form_generation_time']))exceptKeyError:# Backward and tests compatibility : if no timestamp consider edition OKreturnifreq.execute("Any X WHERE X modification_date > %(fts)s, X eid %(eid)s",{'eid':eid,'fts':form_ts}):# We only mark the message for translation but the actual# translation will be handled by the Validation mechanism...msg=_("Entity %(eid)s has changed since you started to edit it."" Reload the page and reapply your changes.")# ... this is why we pass the formats' dict as a third argument.raiseValidationError(eid,{None:msg},{'eid':eid})def_action_apply(self):self._default_publish()self.reset()def_action_cancel(self):errorurl=self._cw.form.get('__errorurl')iferrorurl:self._cw.cancel_edition(errorurl)self._cw.set_message(self._cw._('edit canceled'))returnself.reset()def_action_delete(self):self.delete_entities(self._cw.edited_eids(withtype=True))returnself.reset()