[web] Exposes the undo feature to user through a undo-history view (closes #893940)
# copyright 2003-2011 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/>."""provide an abstract class for external sources using a sqlite database helper"""__docformat__="restructuredtext en"fromos.pathimportjoin,existsfromcubicwebimportserverfromcubicweb.server.sqlutilsimportSQL_PREFIX,SQLAdapterMixIn,sqlexecfromcubicweb.server.sourcesimportnative,rql2sqlfromcubicweb.server.sourcesimportAbstractSource,dbg_st_search,dbg_resultsclassConnectionWrapper(object):def__init__(self,source=None):self.source=sourceself._cnx=Nonedefcursor(self):ifself._cnxisNone:self._cnx=self.source._sqlcnxifserver.DEBUG&server.DBG_SQL:print'sql cnx OPEN',self._cnxreturnself._cnx.cursor()defcommit(self):ifself._cnxisnotNone:ifserver.DEBUG&(server.DBG_SQL|server.DBG_RQL):print'sql cnx COMMIT',self._cnxself._cnx.commit()defrollback(self):ifself._cnxisnotNone:ifserver.DEBUG&(server.DBG_SQL|server.DBG_RQL):print'sql cnx ROLLBACK',self._cnxself._cnx.rollback()defclose(self):ifself._cnxisnotNone:ifserver.DEBUG&server.DBG_SQL:print'sql cnx CLOSE',self._cnxself._cnx.close()self._cnx=NoneclassSQLiteAbstractSource(AbstractSource):"""an abstract class for external sources using a sqlite database helper """sqlgen_class=rql2sql.SQLGenerator@classmethoddefset_nonsystem_types(cls):# those entities are only in this source, we don't want them in the# system sourceforetypeincls.support_entities:native.NONSYSTEM_ETYPES.add(etype)forrtypeincls.support_relations:native.NONSYSTEM_RELATIONS.add(rtype)options=(('helper-db-path',{'type':'string','default':None,'help':'path to the sqlite database file used to do queries on the \repository.','level':2,}),)def__init__(self,repo,appschema,source_config,*args,**kwargs):# the helper db is used to easy querying and will store everything but# actual file contentdbpath=source_config.get('helper-db-path')ifdbpathisNone:dbpath=join(repo.config.appdatahome,'%(uri)s.sqlite'%source_config)self.dbpath=dbpathself.sqladapter=SQLAdapterMixIn({'db-driver':'sqlite','db-name':dbpath})# those attributes have to be initialized before ancestor's __init__# which will call set_schemaself._need_sql_create=notexists(dbpath)self._need_full_import=self._need_sql_createAbstractSource.__init__(self,repo,appschema,source_config,*args,**kwargs)defbackup(self,backupfile,confirm):"""method called to create a backup of the source's data"""self.close_source_connections()try:self.sqladapter.backup_to_file(backupfile,confirm)finally:self.open_source_connections()defrestore(self,backupfile,confirm,drop):"""method called to restore a backup of source's data"""self.close_source_connections()try:self.sqladapter.restore_from_file(backupfile,confirm,drop)finally:self.open_source_connections()@propertydef_sqlcnx(self):# XXX: sqlite connections can only be used in the same thread, so# create a new one each time necessary. If it appears to be time# consuming, find another wayreturnself.sqladapter.get_connection()def_is_schema_complete(self):foretypeinself.support_entities:ifnotetypeinself.schema:self.warning('not ready to generate %s database, %s support missing from schema',self.uri,etype)returnFalseforrtypeinself.support_relations:ifnotrtypeinself.schema:self.warning('not ready to generate %s database, %s support missing from schema',self.uri,rtype)returnFalsereturnTruedef_create_database(self):fromyams.schema2sqlimporteschema2sql,rschema2sqlfromcubicweb.toolsutilsimportrestrict_perms_to_userself.warning('initializing sqlite database for %s source'%self.uri)cnx=self._sqlcnxcu=cnx.cursor()schema=self.schemaforetypeinself.support_entities:eschema=schema.eschema(etype)createsqls=eschema2sql(self.sqladapter.dbhelper,eschema,skip_relations=('data',),prefix=SQL_PREFIX)sqlexec(createsqls,cu,withpb=False)forrtypeinself.support_relations:rschema=schema.rschema(rtype)ifnotrschema.inlined:sqlexec(rschema2sql(rschema),cu,withpb=False)cnx.commit()cnx.close()self._need_sql_create=Falseifself.repo.config['uid']:fromlogilab.common.shellutilsimportchown# database file must be owned by the uid of the server processself.warning('set %s as owner of the database file',self.repo.config['uid'])chown(self.dbpath,self.repo.config['uid'])restrict_perms_to_user(self.dbpath,self.info)defset_schema(self,schema):super(SQLiteAbstractSource,self).set_schema(schema)ifself._need_sql_createandself._is_schema_complete()andself.dbpath:self._create_database()self.rqlsqlgen=self.sqlgen_class(schema,self.sqladapter.dbhelper)defget_connection(self):returnConnectionWrapper(self)defcheck_connection(self,cnx):"""check connection validity, return None if the connection is still valid else a new connection (called when the connections set holding the given connection is being attached to a session) always return the connection to reset eventually cached cursor """returncnxdefcnxset_freed(self,cnx):"""the connections set holding the given connection is being freed from its current attached session: release the connection lock if the connection wrapper has a connection set """# reset _cnx to ensure next thread using cnx will get a new# connectioncnx.close()defsyntax_tree_search(self,session,union,args=None,cachekey=None,varmap=None):"""return result from this source for a rql query (actually from a rql syntax tree and a solution dictionary mapping each used variable to a possible type). If cachekey is given, the query necessary to fetch the results (but not the results themselves) may be cached using this key. """ifself._need_sql_create:return[]assertdbg_st_search(self.uri,union,varmap,args,cachekey)sql,qargs,cbs=self.rqlsqlgen.generate(union,args)args=self.sqladapter.merge_args(args,qargs)cursor=self.doexec(session,sql,args)results=self.sqladapter.process_result(cursor,cbs)assertdbg_results(results)returnresultsdeflocal_add_entity(self,session,entity):"""insert the entity in the local database. This is not provided as add_entity implementation since usually source don't want to simply do this, so let raise NotImplementedError and the source implementor may use this method if necessary """attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.insert(SQL_PREFIX+str(entity.e_schema),attrs)self.doexec(session,sql,attrs)defadd_entity(self,session,entity):"""add a new entity to the source"""raiseNotImplementedError()deflocal_update_entity(self,session,entity,attrs=None):"""update an entity in the source This is not provided as update_entity implementation since usually source don't want to simply do this, so let raise NotImplementedError and the source implementor may use this method if necessary """ifattrsisNone:attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.update(SQL_PREFIX+str(entity.e_schema),attrs,[SQL_PREFIX+'eid'])self.doexec(session,sql,attrs)defupdate_entity(self,session,entity):"""update an entity in the source"""raiseNotImplementedError()defdelete_entity(self,session,entity):"""delete an entity from the source this is not deleting a file in the svn but deleting entities from the source. Main usage is to delete repository content when a Repository entity is deleted. """attrs={'cw_eid':entity.eid}sql=self.sqladapter.sqlgen.delete(SQL_PREFIX+entity.__regid__,attrs)self.doexec(session,sql,attrs)deflocal_add_relation(self,session,subject,rtype,object):"""add a relation to the source This is not provided as add_relation implementation since usually source don't want to simply do this, so let raise NotImplementedError and the source implementor may use this method if necessary """attrs={'eid_from':subject,'eid_to':object}sql=self.sqladapter.sqlgen.insert('%s_relation'%rtype,attrs)self.doexec(session,sql,attrs)defadd_relation(self,session,subject,rtype,object):"""add a relation to the source"""raiseNotImplementedError()defdelete_relation(self,session,subject,rtype,object):"""delete a relation from the source"""rschema=self.schema.rschema(rtype)ifrschema.inlined:ifsubjectinsession.transaction_data.get('pendingeids',()):returntable=SQL_PREFIX+session.describe(subject)[0]column=SQL_PREFIX+rtypesql='UPDATE %s SET %s=NULL WHERE %seid=%%(eid)s'%(table,column,SQL_PREFIX)attrs={'eid':subject}else:attrs={'eid_from':subject,'eid_to':object}sql=self.sqladapter.sqlgen.delete('%s_relation'%rtype,attrs)self.doexec(session,sql,attrs)defdoexec(self,session,query,args=None):"""Execute a query. it's a function just so that it shows up in profiling """ifserver.DEBUG:print'exec',query,argscursor=session.cnxset[self.uri]try:# str(query) to avoid error if it's an unicode stringcursor.execute(str(query),args)exceptException,ex:self.critical("sql: %r\n args: %s\ndbms message: %r",query,args,ex.args[0])try:session.cnxset.connection(self.uri).rollback()self.critical('transaction has been rollbacked')exceptException:passraisereturncursor