| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| try: | ||
| from . import resources_rc | ||
| except ImportError: | ||
| pass | ||
|
|
||
| class DBManagerPlugin: | ||
| def __init__(self, iface): | ||
| self.iface = iface | ||
| self.dlg = None | ||
|
|
||
| def initGui(self): | ||
| self.action = QAction( QIcon(":/db_manager/icon"), u"DB Manager", self.iface.mainWindow() ) | ||
| QObject.connect( self.action, SIGNAL( "triggered()" ), self.run ) | ||
| # Add toolbar button and menu item | ||
| if hasattr( self.iface, 'addDatabaseToolBarIcon' ): | ||
| self.iface.addDatabaseToolBarIcon(self.action) | ||
| else: | ||
| self.iface.addToolBarIcon(self.action) | ||
| if hasattr( self.iface, 'addPluginToDatabaseMenu' ): | ||
| self.iface.addPluginToDatabaseMenu( u"DB Manager", self.action ) | ||
| else: | ||
| self.iface.addPluginToMenu( u"DB Manager", self.action ) | ||
|
|
||
| def unload(self): | ||
| # Remove the plugin menu item and icon | ||
| if hasattr( self.iface, 'removePluginDatabaseMenu' ): | ||
| self.iface.removePluginDatabaseMenu( u"DB Manager", self.action ) | ||
| else: | ||
| self.iface.removePluginMenu( u"DB Manager", self.action ) | ||
| if hasattr( self.iface, 'removeDatabaseToolBarIcon' ): | ||
| self.iface.removeDatabaseToolBarIcon(self.action) | ||
| else: | ||
| self.iface.removeToolBarIcon(self.action) | ||
|
|
||
| if self.dlg != None: | ||
| self.dlg.close() | ||
|
|
||
| def run(self): | ||
| # keep opened only one instance | ||
| if self.dlg == None: | ||
| from db_manager import DBManager | ||
| self.dlg = DBManager(self.iface, self.iface.mainWindow()) | ||
| QObject.connect(self.dlg, SIGNAL("destroyed(QObject *)"), self.onDestroyed) | ||
| self.dlg.show() | ||
| self.dlg.raise_() | ||
| self.dlg.activateWindow() | ||
|
|
||
| def onDestroyed(self, obj): | ||
| self.dlg = None | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| FILE(GLOB PY_FILES *.py) | ||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_PLUGIN_DIR}/db_plugins) | ||
|
|
||
| ADD_SUBDIRECTORY(postgis) | ||
| ADD_SUBDIRECTORY(spatialite) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| class NotSupportedDbType(Exception): | ||
| def __init__(self, dbtype): | ||
| self.msg = u"%s is not supported yet" % dbtype | ||
| Exception(self, self.msg) | ||
|
|
||
| def __str__(self): | ||
| return self.msg.encode('utf-8') | ||
|
|
||
|
|
||
| def initDbPluginList(): | ||
| import os | ||
| current_dir = os.path.dirname(__file__) | ||
| for name in os.listdir(current_dir): | ||
| if not os.path.isdir( os.path.join( current_dir, name ) ): | ||
| continue | ||
|
|
||
| try: | ||
| exec( u"from .%s import plugin as mod" % name ) | ||
| except ImportError, e: | ||
| DBPLUGIN_ERRORS.append( u"%s: %s" % (name, e.message) ) | ||
| continue | ||
|
|
||
| pluginclass = mod.classFactory() | ||
| SUPPORTED_DBTYPES[ pluginclass.typeName() ] = pluginclass | ||
|
|
||
| return len(SUPPORTED_DBTYPES) > 0 | ||
|
|
||
| def supportedDbTypes(): | ||
| return sorted(SUPPORTED_DBTYPES.keys()) | ||
|
|
||
| def getDbPluginErrors(): | ||
| return DBPLUGIN_ERRORS | ||
|
|
||
| def createDbPlugin(dbtype, conn_name=None): | ||
| if not SUPPORTED_DBTYPES.has_key( dbtype ): | ||
| raise NotSupportedDbType( dbtype ) | ||
| dbplugin = SUPPORTED_DBTYPES[ dbtype ] | ||
| return dbplugin if conn_name is None else dbplugin(conn_name) | ||
|
|
||
|
|
||
| # initialize the plugin list | ||
| SUPPORTED_DBTYPES = {} | ||
| DBPLUGIN_ERRORS = [] | ||
| initDbPluginList() | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from qgis.core import QgsDataSourceURI | ||
|
|
||
| from .plugin import DbError, ConnectionError | ||
|
|
||
| class DBConnector: | ||
| def __init__(self, uri): | ||
| self.connection = None | ||
| self._uri = uri | ||
|
|
||
| def __del__(self): | ||
| pass #print "DBConnector.__del__", self._uri.connectionInfo() | ||
| if self.connection != None: | ||
| self.connection.close() | ||
| self.connection = None | ||
|
|
||
|
|
||
| def uri(self): | ||
| return QgsDataSourceURI( self._uri.uri() ) | ||
|
|
||
| def publicUri(self): | ||
| publicUri = QgsDataSourceURI.removePassword( self._uri.uri() ) | ||
| return QgsDataSourceURI( publicUri ) | ||
|
|
||
|
|
||
| def hasSpatialSupport(self): | ||
| return False | ||
|
|
||
| def hasRasterSupport(self): | ||
| return False | ||
|
|
||
| def hasCustomQuerySupport(self): | ||
| return False | ||
|
|
||
| def hasTableColumnEditingSupport(self): | ||
| return False | ||
|
|
||
|
|
||
| def execution_error_types(): | ||
| raise Exception("DBConnector.execution_error_types() is an abstract method") | ||
|
|
||
| def connection_error_types(): | ||
| raise Exception("DBConnector.connection_error_types() is an abstract method") | ||
|
|
||
| def error_types(): | ||
| return self.connection_error_types() + self.execution_error_types() | ||
|
|
||
| def _execute(self, cursor, sql): | ||
| if cursor == None: | ||
| cursor = self._get_cursor() | ||
| try: | ||
| cursor.execute(unicode(sql)) | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e, sql) | ||
|
|
||
| return cursor | ||
|
|
||
| def _execute_and_commit(self, sql): | ||
| """ tries to execute and commit some action, on error it rolls back the change """ | ||
| self._execute(None, sql) | ||
| self._commit() | ||
|
|
||
| def _get_cursor(self, name=None): | ||
| try: | ||
| if name != None: | ||
| name = QString( unicode(name).encode('ascii', 'replace') ).replace( QRegExp("\W"), "_" ).toAscii() | ||
| self._last_cursor_named_id = 0 if not hasattr(self, '_last_cursor_named_id') else self._last_cursor_named_id + 1 | ||
| return self.connection.cursor( "%s_%d" % (name, self._last_cursor_named_id) ) | ||
|
|
||
| return self.connection.cursor() | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e) | ||
|
|
||
| def _close_cursor(self, c): | ||
| try: | ||
| if c and not c.closed: | ||
| c.close() | ||
|
|
||
| except self.error_types(), e: | ||
| pass | ||
|
|
||
| return | ||
|
|
||
|
|
||
| def _fetchall(self, c): | ||
| try: | ||
| return c.fetchall() | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e) | ||
|
|
||
| def _fetchone(self, c): | ||
| try: | ||
| return c.fetchone() | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e) | ||
|
|
||
|
|
||
| def _commit(self): | ||
| try: | ||
| self.connection.commit() | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e) | ||
|
|
||
|
|
||
| def _rollback(self): | ||
| try: | ||
| self.connection.rollback() | ||
|
|
||
| except self.connection_error_types(), e: | ||
| raise ConnectionError(e) | ||
|
|
||
| except self.execution_error_types(), e: | ||
| # do the rollback to avoid a "current transaction aborted, commands ignored" errors | ||
| self._rollback() | ||
| raise DbError(e) | ||
|
|
||
|
|
||
| def _get_cursor_columns(self, c): | ||
| try: | ||
| if c.description: | ||
| return map(lambda x: x[0], c.description) | ||
|
|
||
| except self.connection_error_types() + self.execution_error_types(), e: | ||
| return [] | ||
|
|
||
|
|
||
| @classmethod | ||
| def quoteId(self, identifier): | ||
| if hasattr(identifier, '__iter__'): | ||
| ids = list() | ||
| for i in identifier: | ||
| if i == None: | ||
| continue | ||
| ids.append( self.quoteId(i) ) | ||
| return u'.'.join( ids ) | ||
|
|
||
| identifier = unicode(identifier) if identifier != None else unicode() # make sure it's python unicode string | ||
| return u'"%s"' % identifier.replace('"', '""') | ||
|
|
||
| @classmethod | ||
| def quoteString(self, txt): | ||
| """ make the string safe - replace ' with '' """ | ||
| if hasattr(txt, '__iter__'): | ||
| txts = list() | ||
| for i in txt: | ||
| if i == None: | ||
| continue | ||
| txts.append( self.quoteString(i) ) | ||
| return u'.'.join( txts ) | ||
|
|
||
| txt = unicode(txt) if txt != None else unicode() # make sure it's python unicode string | ||
| return u"'%s'" % txt.replace("'", "''") | ||
|
|
||
| @classmethod | ||
| def getSchemaTableName(self, table): | ||
| if not hasattr(table, '__iter__'): | ||
| return (None, table) | ||
| elif len(table) < 2: | ||
| return (None, table[0]) | ||
| else: | ||
| return (table[0], table[1]) | ||
|
|
||
| @classmethod | ||
| def getSqlDictionary(self): | ||
| """ return a generic SQL dictionary """ | ||
| try: | ||
| from ..sql_dictionary import getSqlDictionary | ||
| return getSqlDictionary() | ||
| except ImportError: | ||
| return [] | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from .plugin import DbError | ||
|
|
||
| class BaseTableModel(QAbstractTableModel): | ||
| def __init__(self, header=None, data=None, parent=None): | ||
| QAbstractTableModel.__init__(self, parent) | ||
| self._header = header if header else [] | ||
| self.resdata = data if data else [] | ||
|
|
||
| def headerToString(self, sep=u"\t"): | ||
| header = QStringList() << self._header | ||
| return header.join( sep ) | ||
|
|
||
| def rowToString(self, row, sep=u"\t"): | ||
| text = QString() | ||
| for col in range(self.columnCount()): | ||
| text += u"%s" % self.getData(row, col) + sep | ||
| return text[:-1] | ||
|
|
||
| def getData(self, row, col): | ||
| return self.resdata[row][col] | ||
|
|
||
| def columnNames(self): | ||
| return list(self._header) | ||
|
|
||
| def rowCount(self, parent=None): | ||
| return len(self.resdata) | ||
|
|
||
| def columnCount(self, parent=None): | ||
| return len(self._header) | ||
|
|
||
| def data(self, index, role): | ||
| if role != Qt.DisplayRole and role != Qt.FontRole: | ||
| return QVariant() | ||
|
|
||
| val = self.getData(index.row(), index.column()) | ||
|
|
||
| if role == Qt.FontRole: # draw NULL in italic | ||
| if val != None: | ||
| return QVariant() | ||
| f = QFont() | ||
| f.setItalic(True) | ||
| return QVariant(f) | ||
|
|
||
| if val == None: | ||
| return QVariant("NULL") | ||
| elif isinstance(val, buffer): | ||
| # hide binary data | ||
| return QVariant() | ||
| elif isinstance(val, (str, unicode, QString)) and len(val) > 300: | ||
| # too much data to display, elide the string | ||
| return QVariant( u"%s..." % val[:300] ) | ||
| return QVariant( unicode(val) ) # convert to string | ||
|
|
||
| def headerData(self, section, orientation, role): | ||
| if role != Qt.DisplayRole: | ||
| return QVariant() | ||
|
|
||
| if orientation == Qt.Vertical: | ||
| # header for a row | ||
| return QVariant(section+1) | ||
| else: | ||
| # header for a column | ||
| return QVariant(self._header[section]) | ||
|
|
||
|
|
||
| class TableDataModel(BaseTableModel): | ||
| def __init__(self, table, parent=None): | ||
| self.db = table.database().connector | ||
| self.table = table | ||
|
|
||
| fieldNames = map(lambda x: x.name, table.fields()) | ||
| BaseTableModel.__init__(self, fieldNames, None, parent) | ||
|
|
||
| # get table fields | ||
| self.fields = [] | ||
| for fld in table.fields(): | ||
| self.fields.append( self._sanitizeTableField(fld) ) | ||
|
|
||
| self.fetchedCount = 201 | ||
| self.fetchedFrom = -self.fetchedCount-1 # so the first call to getData will exec fetchMoreData(0) | ||
|
|
||
| def _sanitizeTableField(self, field): | ||
| """ quote column names to avoid some problems (e.g. columns with upper case) """ | ||
| return self.db.quoteId(field) | ||
|
|
||
| def getData(self, row, col): | ||
| if row < self.fetchedFrom or row >= self.fetchedFrom + self.fetchedCount: | ||
| margin = self.fetchedCount/2 | ||
| start = self.rowCount() - margin if row + margin >= self.rowCount() else row - margin | ||
| if start < 0: start = 0 | ||
| self.fetchMoreData(start) | ||
| return self.resdata[row-self.fetchedFrom][col] | ||
|
|
||
| def fetchMoreData(self, row_start): | ||
| pass | ||
|
|
||
| def rowCount(self, index=None): | ||
| # case for tables with no columns ... any reason to use them? :-) | ||
| return self.table.rowCount if self.table.rowCount != None and self.columnCount(index) > 0 else 0 | ||
|
|
||
|
|
||
| class SqlResultModel(BaseTableModel): | ||
| def __init__(self, db, sql, parent=None): | ||
| self.db = db.connector | ||
|
|
||
| t = QTime() | ||
| t.start() | ||
| c = self.db._execute(None, unicode(sql)) | ||
| self._secs = t.elapsed() / 1000.0 | ||
| del t | ||
|
|
||
| self._affectedRows = 0 | ||
| data = [] | ||
| header = self.db._get_cursor_columns(c) | ||
| if header == None: | ||
| header = [] | ||
|
|
||
| try: | ||
| if len(header) > 0: | ||
| data = self.db._fetchall(c) | ||
| self._affectedRows = c.rowcount | ||
| except DbError: | ||
| # nothing to fetch! | ||
| data = [] | ||
| header = [] | ||
|
|
||
| BaseTableModel.__init__(self, header, data, parent) | ||
|
|
||
| # commit before closing the cursor to make sure that the changes are stored | ||
| self.db._commit() | ||
| c.close() | ||
| del c | ||
|
|
||
| def secs(self): | ||
| return self._secs | ||
|
|
||
| def affectedRows(self): | ||
| return self._affectedRows | ||
|
|
||
|
|
||
|
|
||
| class SimpleTableModel(QStandardItemModel): | ||
| def __init__(self, header, editable=False, parent=None): | ||
| self.header = header | ||
| self.editable = editable | ||
| QStandardItemModel.__init__(self, 0, len(self.header), parent) | ||
|
|
||
| def rowFromData(self, data): | ||
| row = [] | ||
| for c in data: | ||
| item = QStandardItem(unicode(c)) | ||
| item.setFlags( (item.flags() | Qt.ItemIsEditable) if self.editable else (item.flags() & ~Qt.ItemIsEditable) ) | ||
| row.append( item ) | ||
| return row | ||
|
|
||
| def headerData(self, section, orientation, role): | ||
| if orientation == Qt.Horizontal and role == Qt.DisplayRole: | ||
| return QVariant(self.header[section]) | ||
| return QVariant() | ||
|
|
||
| def _getNewObject(self): | ||
| pass | ||
|
|
||
| def getObject(self, row): | ||
| return self._getNewObject() | ||
|
|
||
| def getObjectIter(self): | ||
| for row in range(self.rowCount()): | ||
| yield self.getObject(row) | ||
|
|
||
|
|
||
|
|
||
| class TableFieldsModel(SimpleTableModel): | ||
| def __init__(self, parent, editable=False): | ||
| SimpleTableModel.__init__(self, ['Name', 'Type', 'Null', 'Default'], editable, parent) | ||
|
|
||
| def headerData(self, section, orientation, role): | ||
| if orientation == Qt.Vertical and role == Qt.DisplayRole: | ||
| return QVariant(section+1) | ||
| return SimpleTableModel.headerData(self, section, orientation, role) | ||
|
|
||
| def append(self, fld): | ||
| data = [fld.name, fld.type2String(), not fld.notNull, fld.default2String()] | ||
| self.appendRow( self.rowFromData(data) ) | ||
| row = self.rowCount()-1 | ||
| self.setData(self.index(row, 0), QVariant(fld), Qt.UserRole) | ||
| self.setData(self.index(row, 1), QVariant(fld.primaryKey), Qt.UserRole) | ||
|
|
||
| def _getNewObject(self): | ||
| from .plugin import TableField | ||
| return TableField(None) | ||
|
|
||
| def getObject(self, row): | ||
| val = self.data(self.index(row, 0), Qt.UserRole) | ||
| fld = val.toPyObject() if val.isValid() else self._getNewObject() | ||
| fld.name = self.data(self.index(row, 0)).toString() | ||
|
|
||
| typestr = self.data(self.index(row, 1)).toString() | ||
| regex = QRegExp( "([^\(]+)\(([^\)]+)\)" ) | ||
| startpos = regex.indexIn( QString(typestr) ) | ||
| if startpos >= 0: | ||
| fld.dataType = regex.cap(1).trimmed() | ||
| fld.modifier = regex.cap(2).trimmed() | ||
| else: | ||
| fld.modifier = None | ||
| fld.dataType = typestr | ||
|
|
||
| fld.notNull = not self.data(self.index(row, 2)).toBool() | ||
| fld.primaryKey = self.data(self.index(row, 1), Qt.UserRole).toBool() | ||
| return fld | ||
|
|
||
| def getFields(self): | ||
| flds = [] | ||
| for fld in self.getObjectIter(): | ||
| flds.append( fld ) | ||
| return flds | ||
|
|
||
|
|
||
| class TableConstraintsModel(SimpleTableModel): | ||
| def __init__(self, parent, editable=False): | ||
| SimpleTableModel.__init__(self, ['Name', 'Type', 'Column(s)'], editable, parent) | ||
|
|
||
| def append(self, constr): | ||
| field_names = map( lambda (k,v): unicode(v.name), constr.fields().iteritems() ) | ||
| data = [constr.name, constr.type2String(), u", ".join(field_names)] | ||
| self.appendRow( self.rowFromData(data) ) | ||
| row = self.rowCount()-1 | ||
| self.setData(self.index(row, 0), QVariant(constr), Qt.UserRole) | ||
| self.setData(self.index(row, 1), QVariant(constr.type), Qt.UserRole) | ||
| self.setData(self.index(row, 2), QVariant(constr.columns), Qt.UserRole) | ||
|
|
||
| def _getNewObject(self): | ||
| from .plugin import TableConstraint | ||
| return TableConstraint(None) | ||
|
|
||
| def getObject(self, row): | ||
| val = self.data(self.index(row, 0), Qt.UserRole) | ||
| constr = val.toPyObject() if val.isValid() else self._getNewObject() | ||
| constr.name = self.data(self.index(row, 0)).toString() | ||
| constr.type = self.data(self.index(row, 1), Qt.UserRole).toInt()[0] | ||
| constr.columns = self.data(self.index(row, 2), Qt.UserRole).toList() | ||
| return constr | ||
|
|
||
| def getConstraints(self): | ||
| constrs = [] | ||
| for constr in self.getObjectIter(): | ||
| constrs.append( constr ) | ||
| return constrs | ||
|
|
||
|
|
||
| class TableIndexesModel(SimpleTableModel): | ||
| def __init__(self, parent, editable=False): | ||
| SimpleTableModel.__init__(self, ['Name', 'Column(s)'], editable, parent) | ||
|
|
||
| def append(self, idx): | ||
| field_names = map( lambda (k,v): unicode(v.name), idx.fields().iteritems() ) | ||
| data = [idx.name, u", ".join(field_names)] | ||
| self.appendRow( self.rowFromData(data) ) | ||
| row = self.rowCount()-1 | ||
| self.setData(self.index(row, 0), QVariant(idx), Qt.UserRole) | ||
| self.setData(self.index(row, 1), QVariant(idx.columns), Qt.UserRole) | ||
|
|
||
| def _getNewObject(self): | ||
| from .plugin import TableIndex | ||
| return TableIndex(None) | ||
|
|
||
| def getObject(self, row): | ||
| val = self.data(self.index(row, 0), Qt.UserRole) | ||
| idx = val.toPyObject() if val.isValid() else self._getNewObject() | ||
| idx.name = self.data(self.index(row, 0)).toString() | ||
| idx.columns = self.data(self.index(row, 1), Qt.UserRole).toList() | ||
| return idx | ||
|
|
||
| def getIndexes(self): | ||
| idxs = [] | ||
| for idx in self.getObjectIter(): | ||
| idxs.append( idx ) | ||
| return idxs | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| class HtmlContent: | ||
| def __init__(self, data): | ||
| self.data = data if not isinstance(data, HtmlContent) else data.data | ||
|
|
||
| def toHtml(self): | ||
| if isinstance(self.data, list) or isinstance(self.data, tuple): | ||
| html = u'' | ||
| for item in self.data: | ||
| html += HtmlContent(item).toHtml() | ||
| return html | ||
|
|
||
| if hasattr(self.data, 'toHtml' ): | ||
| return self.data.toHtml() | ||
|
|
||
| html = unicode(self.data).replace("\n", "<br>") | ||
| return html | ||
|
|
||
| def hasContents(self): | ||
| if isinstance(self.data, list) or isinstance(self.data, tuple): | ||
| empty = True | ||
| for item in self.data: | ||
| if item.hasContents(): | ||
| empty = False | ||
| break | ||
| return not empty | ||
|
|
||
| if hasattr(self.data, 'hasContents'): | ||
| return self.data.hasContents() | ||
|
|
||
| return len(self.data) > 0 | ||
|
|
||
| class HtmlElem: | ||
| def __init__(self, tag, data, attrs=None): | ||
| self.tag = tag | ||
| self.data = data if isinstance(data, HtmlContent) else HtmlContent(data) | ||
| self.attrs = attrs if attrs != None else dict() | ||
| if self.attrs.has_key('tag'): | ||
| self.setTag( self.attrs['tag'] ) | ||
| del self.attrs['tag'] | ||
|
|
||
| def setTag(self, tag): | ||
| self.tag = tag | ||
|
|
||
| def getOriginalData(self): | ||
| return self.data.data | ||
|
|
||
| def setAttr(self, name, value): | ||
| self.attrs[name] = value | ||
|
|
||
| def getAttrsHtml(self): | ||
| html = u'' | ||
| for k, v in self.attrs.iteritems(): | ||
| html += u' %s="%s"' % ( k, QStringList(v).join(' ') ) | ||
| return html | ||
|
|
||
| def openTagHtml(self): | ||
| return u"<%s%s>" % ( self.tag, self.getAttrsHtml() ) | ||
|
|
||
| def closeTagHtml(self): | ||
| return u"</%s>" % self.tag | ||
|
|
||
| def toHtml(self): | ||
| return u"%s%s%s" % ( self.openTagHtml(), self.data.toHtml(), self.closeTagHtml() ) | ||
|
|
||
| def hasContents(self): | ||
| return self.data.toHtml() != "" | ||
|
|
||
|
|
||
| class HtmlParagraph(HtmlElem): | ||
| def __init__(self, data, attrs=None): | ||
| HtmlElem.__init__(self, 'p', data, attrs) | ||
|
|
||
|
|
||
| class HtmlListItem(HtmlElem): | ||
| def __init__(self, data, attrs=None): | ||
| HtmlElem.__init__(self, 'li', data, attrs) | ||
|
|
||
| class HtmlList(HtmlElem): | ||
| def __init__(self, items, attrs=None): | ||
| # make sure to have HtmlListItem items | ||
| items = list(items) | ||
| for i, item in enumerate(items): | ||
| if not isinstance(item, HtmlListItem): | ||
| items[i] = HtmlListItem( item ) | ||
| HtmlElem.__init__(self, 'ul', items, attrs) | ||
|
|
||
|
|
||
| class HtmlTableCol(HtmlElem): | ||
| def __init__(self, data, attrs=None): | ||
| HtmlElem.__init__(self, 'td', data, attrs) | ||
|
|
||
| def closeTagHtml(self): | ||
| # FIX INVALID BEHAVIOR: an empty cell as last table's cell break margins | ||
| return u" %s" % HtmlElem.closeTagHtml(self) | ||
|
|
||
| class HtmlTableRow(HtmlElem): | ||
| def __init__(self, cols, attrs=None): | ||
| # make sure to have HtmlTableCol items | ||
| cols = list(cols) | ||
| for i, c in enumerate(cols): | ||
| if not isinstance(c, HtmlTableCol): | ||
| cols[i] = HtmlTableCol( c ) | ||
| HtmlElem.__init__(self, 'tr', cols, attrs) | ||
|
|
||
| class HtmlTableHeader(HtmlTableRow): | ||
| def __init__(self, cols, attrs=None): | ||
| HtmlTableRow.__init__(self, cols, attrs) | ||
| for c in self.getOriginalData(): | ||
| c.setTag('th') | ||
|
|
||
| class HtmlTable(HtmlElem): | ||
| def __init__(self, rows, attrs=None): | ||
| # make sure to have HtmlTableRow items | ||
| rows = list(rows) | ||
| for i, r in enumerate(rows): | ||
| if not isinstance(r, HtmlTableRow): | ||
| rows[i] = HtmlTableRow( r ) | ||
| HtmlElem.__init__(self, 'table', rows, attrs) | ||
|
|
||
|
|
||
| class HtmlWarning(HtmlContent): | ||
| def __init__(self, data): | ||
| data = [ '<img src=":/icons/warning-20px.png"> ', data ] | ||
| HtmlContent.__init__(self, data) | ||
|
|
||
|
|
||
| class HtmlSection(HtmlContent): | ||
| def __init__(self, title, content=None): | ||
| data = [ '<div class="section"><h2>', title, '</h2>' ] | ||
| if content != None: | ||
| data.extend( [ '<div>', content, '</div>' ] ) | ||
| data.append( '</div>' ) | ||
| HtmlContent.__init__(self, data) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| SET (DB_MANAGER_POSTGIS_DIR ${DB_MANAGER_PLUGIN_DIR}/db_plugins/postgis) | ||
|
|
||
| FILE(GLOB PY_FILES *.py) | ||
| FILE(GLOB ICON_FILES icons/*.png) | ||
|
|
||
| PYQT4_ADD_RESOURCES(PYRC_FILES resources.qrc) | ||
| ADD_CUSTOM_TARGET(db_manager_postgis ALL DEPENDS ${PYRC_FILES}) | ||
|
|
||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_POSTGIS_DIR}) | ||
| INSTALL(FILES ${PYRC_FILES} DESTINATION ${DB_MANAGER_POSTGIS_DIR}) | ||
| INSTALL(FILES ${ICON_FILES} DESTINATION ${DB_MANAGER_POSTGIS_DIR}/icons) | ||
|
|
||
| ADD_SUBDIRECTORY(plugins) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from ..data_model import TableDataModel, SqlResultModel | ||
| from ..plugin import BaseError | ||
|
|
||
| class PGTableDataModel(TableDataModel): | ||
| def __init__(self, table, parent=None): | ||
| self.cursor = None | ||
| TableDataModel.__init__(self, table, parent) | ||
|
|
||
| if self.table.rowCount == None: | ||
| self.table.refreshRowCount() | ||
| if self.table.rowCount == None: | ||
| return | ||
|
|
||
| self.connect(self.table, SIGNAL("aboutToChange"), self._deleteCursor) | ||
| self._createCursor() | ||
|
|
||
| def _createCursor(self): | ||
| fields_txt = u", ".join(self.fields) | ||
| table_txt = self.db.quoteId( (self.table.schemaName(), self.table.name) ) | ||
|
|
||
| # create named cursor and run query | ||
| self.cursor = self.db._get_cursor(self.table.name) | ||
| sql = u"SELECT %s FROM %s" % (fields_txt, table_txt) | ||
| self.db._execute(self.cursor, sql) | ||
|
|
||
| def _sanitizeTableField(self, field): | ||
| # get fields, ignore geometry columns | ||
| if field.dataType.lower() == "geometry": | ||
| return u"CASE WHEN %(fld)s IS NULL THEN NULL ELSE GeometryType(%(fld)s) END AS %(fld)s" % {'fld': self.db.quoteId(field.name)} | ||
| elif field.dataType.lower() == "raster": | ||
| return u"CASE WHEN %(fld)s IS NULL THEN NULL ELSE 'RASTER' END AS %(fld)s" % {'fld': self.db.quoteId(field.name)} | ||
| return u"%s::text" % self.db.quoteId(field.name) | ||
|
|
||
| def _deleteCursor(self): | ||
| self.db._close_cursor(self.cursor) | ||
| self.cursor = None | ||
|
|
||
| def __del__(self): | ||
| self.disconnect(self.table, SIGNAL("aboutToChange"), self._deleteCursor) | ||
| self._deleteCursor() | ||
| pass #print "PGTableModel.__del__" | ||
|
|
||
| def fetchMoreData(self, row_start): | ||
| if not self.cursor: | ||
| self._createCursor() | ||
|
|
||
| try: | ||
| self.cursor.scroll(row_start, mode='absolute') | ||
| except self.db.error_types(): | ||
| self._deleteCursor() | ||
| return self.fetchMoreData(row_start) | ||
|
|
||
| self.resdata = self.cursor.fetchmany(self.fetchedCount) | ||
| self.fetchedFrom = row_start | ||
|
|
||
|
|
||
| class PGSqlResultModel(SqlResultModel): | ||
| pass | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from ..info_model import TableInfo, VectorTableInfo, RasterTableInfo | ||
| from ..html_elems import HtmlSection, HtmlParagraph, HtmlList, HtmlTable, HtmlTableHeader, HtmlTableCol | ||
|
|
||
| class PGTableInfo(TableInfo): | ||
| def __init__(self, table): | ||
| self.table = table | ||
|
|
||
|
|
||
| def generalInfo(self): | ||
| ret = [] | ||
|
|
||
| # if the estimation is less than 100 rows, try to count them - it shouldn't take long time | ||
| if self.table.rowCount == None and self.table.estimatedRowCount < 100: | ||
| # row count information is not displayed yet, so just block | ||
| # table signals to avoid double refreshing (infoViewer->refreshRowCount->tableChanged->infoViewer) | ||
| self.table.blockSignals(True) | ||
| self.table.refreshRowCount() | ||
| self.table.blockSignals(False) | ||
|
|
||
| tbl = [ | ||
| ("Relation type:", "View" if self.table.isView else "Table"), | ||
| ("Owner:", self.table.owner) | ||
| ] | ||
| if self.table.comment: | ||
| tbl.append( ("Comment:", self.table.comment) ) | ||
|
|
||
| tbl.extend([ | ||
| ("Pages:", self.table.pages), | ||
| ("Rows (estimation):", self.table.estimatedRowCount ) | ||
| ]) | ||
|
|
||
| # privileges | ||
| # has the user access to this schema? | ||
| schema_priv = self.table.database().connector.getSchemaPrivileges(self.table.schemaName()) if self.table.schema() else None | ||
| if schema_priv == None: | ||
| pass | ||
| elif schema_priv[1] == False: # no usage privileges on the schema | ||
| tbl.append( ("Privileges:", u"<warning> This user doesn't have usage privileges for this schema!" ) ) | ||
| else: | ||
| table_priv = self.table.database().connector.getTablePrivileges( (self.table.schemaName(), self.table.name) ) | ||
| privileges = [] | ||
| if table_priv[0]: | ||
| privileges.append("select") | ||
|
|
||
| if self.table.rowCount == None or self.table.rowCount >= 0: | ||
| tbl.append( ("Rows (counted):", self.table.rowCount if self.table.rowCount != None else 'Unknown (<a href="action:rows/count">find out</a>)') ) | ||
|
|
||
| if table_priv[1]: privileges.append("insert") | ||
| if table_priv[2]: privileges.append("update") | ||
| if table_priv[3]: privileges.append("delete") | ||
| priv_string = u", ".join(privileges) if len(privileges) > 0 else u'<warning> This user has no privileges!' | ||
| tbl.append( ("Privileges:", priv_string ) ) | ||
|
|
||
| ret.append( HtmlTable( tbl ) ) | ||
|
|
||
| if schema_priv != None and schema_priv[1]: | ||
| if table_priv[0] and not table_priv[1] and not table_priv[2] and not table_priv[3]: | ||
| ret.append( HtmlParagraph( u"<warning> This user has read-only privileges." ) ) | ||
|
|
||
| if not self.table.isView: | ||
| if self.table.rowCount != None: | ||
| if abs(self.table.estimatedRowCount - self.table.rowCount) > 1 and \ | ||
| (self.table.estimatedRowCount > 2 * self.table.rowCount or \ | ||
| self.table.rowCount > 2 * self.table.estimatedRowCount): | ||
| ret.append( HtmlParagraph( u"<warning> There's a significant difference between estimated and real row count. " \ | ||
| 'Consider running <a href="action:vacuumanalyze/run">VACUUM ANALYZE</a>.' ) ) | ||
|
|
||
| # primary key defined? | ||
| if not self.table.isView: | ||
| if len( filter(lambda fld: fld.primaryKey, self.table.fields()) ) <= 0: | ||
| ret.append( HtmlParagraph( u"<warning> No primary key defined for this table!" ) ) | ||
|
|
||
| return ret | ||
|
|
||
|
|
||
| def fieldsDetails(self): | ||
| tbl = [] | ||
|
|
||
| # define the table header | ||
| header = ( "#", "Name", "Type", "Length", "Null", "Default" ) | ||
| tbl.append( HtmlTableHeader( header ) ) | ||
|
|
||
| # add table contents | ||
| for fld in self.table.fields(): | ||
| char_max_len = fld.charMaxLen if fld.charMaxLen != None and fld.charMaxLen != -1 else "" | ||
| is_null_txt = "N" if fld.notNull else "Y" | ||
|
|
||
| # make primary key field underlined | ||
| attrs = {"class":"underline"} if fld.primaryKey else None | ||
| name = HtmlTableCol( fld.name, attrs ) | ||
|
|
||
| tbl.append( (fld.num, name, fld.type2String(), char_max_len, is_null_txt, fld.default2String()) ) | ||
|
|
||
| return HtmlTable( tbl, {"class":"header"} ) | ||
|
|
||
|
|
||
| def triggersDetails(self): | ||
| if self.table.triggers() == None or len(self.table.triggers()) <= 0: | ||
| return None | ||
|
|
||
| ret = [] | ||
|
|
||
| tbl = [] | ||
| # define the table header | ||
| header = ( "Name", "Function", "Type", "Enabled" ) | ||
| tbl.append( HtmlTableHeader( header ) ) | ||
|
|
||
| # add table contents | ||
| for trig in self.table.triggers(): | ||
| name = u'%(name)s (<a href="action:trigger/%(name)s/%(action)s">%(action)s</a>)' % { "name":trig.name, "action":"delete" } | ||
|
|
||
| (enabled, action) = ("Yes", "disable") if trig.enabled else ("No", "enable") | ||
| txt_enabled = u'%(enabled)s (<a href="action:trigger/%(name)s/%(action)s">%(action)s</a>)' % { "name":trig.name, "action":action, "enabled":enabled } | ||
|
|
||
| tbl.append( (name, trig.function, trig.type2String(), txt_enabled) ) | ||
|
|
||
| ret.append( HtmlTable( tbl, {"class":"header"} ) ) | ||
|
|
||
| ret.append( HtmlParagraph( '<a href="action:triggers/enable">Enable all triggers</a> / <a href="action:triggers/disable">Disable all triggers</a>' ) ) | ||
| return ret | ||
|
|
||
|
|
||
| def rulesDetails(self): | ||
| if self.table.rules() == None or len(self.table.rules()) <= 0: | ||
| return None | ||
|
|
||
| tbl = [] | ||
| # define the table header | ||
| header = ( "Name", "Definition" ) | ||
| tbl.append( HtmlTableHeader( header ) ) | ||
|
|
||
| # add table contents | ||
| for rule in self.table.rules(): | ||
| name = u'%(name)s (<a href="action:rule/%(name)s/%(action)s">%(action)s</a>)' % { "name":rule.name, "action":"delete" } | ||
| tbl.append( (name, rule.definition) ) | ||
|
|
||
| return HtmlTable( tbl, {"class":"header"} ) | ||
|
|
||
|
|
||
| def getTableInfo(self): | ||
| ret = TableInfo.getTableInfo(self) | ||
|
|
||
| # rules | ||
| rules_details = self.rulesDetails() | ||
| if rules_details == None: | ||
| pass | ||
| else: | ||
| ret.append( HtmlSection( 'Rules', rules_details ) ) | ||
|
|
||
| return ret | ||
|
|
||
| class PGVectorTableInfo(PGTableInfo, VectorTableInfo): | ||
| def __init__(self, table): | ||
| VectorTableInfo.__init__(self, table) | ||
| PGTableInfo.__init__(self, table) | ||
|
|
||
| def spatialInfo(self): | ||
| return VectorTableInfo.spatialInfo(self) | ||
|
|
||
| class PGRasterTableInfo(PGTableInfo, RasterTableInfo): | ||
| def __init__(self, table): | ||
| RasterTableInfo.__init__(self, table) | ||
| PGTableInfo.__init__(self, table) | ||
|
|
||
| def spatialInfo(self): | ||
| return RasterTableInfo.spatialInfo(self) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| # this will disable the dbplugin if the connector raise an ImportError | ||
| from .connector import PostGisDBConnector | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from ..plugin import ConnectionError, DBPlugin, Database, Schema, Table, VectorTable, RasterTable, TableField, TableConstraint, TableIndex, TableTrigger, TableRule | ||
| try: | ||
| from . import resources_rc | ||
| except ImportError: | ||
| pass | ||
|
|
||
| from ..html_elems import HtmlParagraph, HtmlList, HtmlTable | ||
|
|
||
|
|
||
| def classFactory(): | ||
| return PostGisDBPlugin | ||
|
|
||
| class PostGisDBPlugin(DBPlugin): | ||
|
|
||
| @classmethod | ||
| def icon(self): | ||
| return QIcon(":/db_manager/postgis/icon") | ||
|
|
||
| @classmethod | ||
| def typeName(self): | ||
| return 'postgis' | ||
|
|
||
| @classmethod | ||
| def typeNameString(self): | ||
| return 'PostGIS' | ||
|
|
||
| @classmethod | ||
| def providerName(self): | ||
| return 'postgres' | ||
|
|
||
| @classmethod | ||
| def connectionSettingsKey(self): | ||
| return '/PostgreSQL/connections' | ||
|
|
||
| def databasesFactory(self, connection, uri): | ||
| return PGDatabase(connection, uri) | ||
|
|
||
| def connect(self, parent=None): | ||
| conn_name = self.connectionName() | ||
| settings = QSettings() | ||
| settings.beginGroup( u"/%s/%s" % (self.connectionSettingsKey(), conn_name) ) | ||
|
|
||
| if not settings.contains( "database" ): # non-existent entry? | ||
| raise InvalidDataException( u'there is no defined database connection "%s".' % conn_name ) | ||
|
|
||
| from qgis.core import QgsDataSourceURI | ||
| uri = QgsDataSourceURI() | ||
|
|
||
| settingsList = ["service", "host", "port", "database", "username", "password"] | ||
| service, host, port, database, username, password = map(lambda x: settings.value(x).toString(), settingsList) | ||
|
|
||
| # qgis1.5 use 'savePassword' instead of 'save' setting | ||
| savedPassword = settings.value("save", False).toBool() or settings.value("savePassword", False).toBool() | ||
|
|
||
| useEstimatedMetadata = settings.value("estimatedMetadata", False).toBool() | ||
| sslmode = settings.value("sslmode", QgsDataSourceURI.SSLprefer).toInt()[0] | ||
|
|
||
| settings.endGroup() | ||
|
|
||
| if not service.isEmpty(): | ||
| uri.setConnection(service, database, username, password, sslmode) | ||
| else: | ||
| uri.setConnection(host, port, database, username, password, sslmode) | ||
|
|
||
| uri.setUseEstimatedMetadata(useEstimatedMetadata) | ||
|
|
||
| err = QString() | ||
| try: | ||
| return self.connectToUri(uri) | ||
| except ConnectionError, e: | ||
| err = QString( str(e) ) | ||
|
|
||
| hasCredentialDlg = True | ||
| try: | ||
| from qgis.gui import QgsCredentials | ||
| except ImportError: # no credential dialog | ||
| hasCredentialDlg = False | ||
|
|
||
| # ask for valid credentials | ||
| max_attempts = 3 | ||
| for i in range(max_attempts): | ||
| if hasCredentialDlg: | ||
| (ok, username, password) = QgsCredentials.instance().get(uri.connectionInfo(), username, password, err) | ||
| else: | ||
| (password, ok) = QInputDialog.getText(parent, u"Enter password", u'Enter password for connection "%s":' % conn_name, QLineEdit.Password) | ||
|
|
||
| if not ok: | ||
| return False | ||
|
|
||
| if not service.isEmpty(): | ||
| uri.setConnection(service, database, username, password, sslmode) | ||
| else: | ||
| uri.setConnection(host, port, database, username, password, sslmode) | ||
|
|
||
| try: | ||
| self.connectToUri(uri) | ||
| except ConnectionError, e: | ||
| if i == max_attempts-1: # failed the last attempt | ||
| raise e | ||
| err = QString( str(e) ) | ||
| continue | ||
|
|
||
| if hasCredentialDlg: | ||
| QgsCredentials.instance().put(uri.connectionInfo(), username, password) | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
|
|
||
| class PGDatabase(Database): | ||
| def __init__(self, connection, uri): | ||
| Database.__init__(self, connection, uri) | ||
|
|
||
| def connectorsFactory(self, uri): | ||
| return PostGisDBConnector(uri) | ||
|
|
||
| def dataTablesFactory(self, row, db, schema=None): | ||
| return PGTable(row, db, schema) | ||
|
|
||
| def vectorTablesFactory(self, row, db, schema=None): | ||
| return PGVectorTable(row, db, schema) | ||
|
|
||
| def rasterTablesFactory(self, row, db, schema=None): | ||
| return PGRasterTable(row, db, schema) | ||
|
|
||
| def schemasFactory(self, row, db): | ||
| return PGSchema(row, db) | ||
|
|
||
| def sqlResultModel(self, sql, parent): | ||
| from .data_model import PGSqlResultModel | ||
| return PGSqlResultModel(self, sql, parent) | ||
|
|
||
|
|
||
| def registerDatabaseActions(self, mainWindow): | ||
| Database.registerDatabaseActions(self, mainWindow) | ||
|
|
||
| # add a separator | ||
| separator = QAction(self); | ||
| separator.setSeparator(True) | ||
| mainWindow.registerAction( separator, "&Table" ) | ||
|
|
||
| action = QAction("Run &Vacuum Analyze", self) | ||
| mainWindow.registerAction( action, "&Table", self.runVacuumAnalyzeActionSlot ) | ||
|
|
||
| def runVacuumAnalyzeActionSlot(self, item, action, parent): | ||
| QApplication.restoreOverrideCursor() | ||
| try: | ||
| if not isinstance(item, Table) or item.isView: | ||
| QMessageBox.information(parent, "Sorry", "Select a TABLE for vacuum analyze.") | ||
| return | ||
| finally: | ||
| QApplication.setOverrideCursor(Qt.WaitCursor) | ||
|
|
||
| item.runVacuumAnalyze() | ||
|
|
||
|
|
||
| class PGSchema(Schema): | ||
| def __init__(self, row, db): | ||
| Schema.__init__(self, db) | ||
| self.oid, self.name, self.owner, self.perms, self.comment = row | ||
|
|
||
|
|
||
| class PGTable(Table): | ||
| def __init__(self, row, db, schema=None): | ||
| Table.__init__(self, db, schema) | ||
| self.name, schema_name, self.isView, self.owner, self.estimatedRowCount, self.pages, self.comment = row | ||
| self.estimatedRowCount = int(self.estimatedRowCount) | ||
|
|
||
| def runVacuumAnalyze(self): | ||
| self.aboutToChange() | ||
| self.database().connector.runVacuumAnalyze( (self.schemaName(), self.name) ) | ||
| # TODO: change only this item, not re-create all the tables in the schema/database | ||
| self.schema().refresh() if self.schema() else self.database().refresh() | ||
|
|
||
| def runAction(self, action): | ||
| action = unicode(action) | ||
|
|
||
| if action.startswith( "vacuumanalyze/" ): | ||
| if action == "vacuumanalyze/run": | ||
| self.runVacuumAnalyze() | ||
| return True | ||
|
|
||
| elif action.startswith( "rule/" ): | ||
| parts = action.split('/') | ||
| rule_name = parts[1] | ||
| rule_action = parts[2] | ||
|
|
||
| msg = u"Do you want to %s rule %s?" % (rule_action, rule_name) | ||
| QApplication.restoreOverrideCursor() | ||
| try: | ||
| if QMessageBox.question(None, "Table rule", msg, QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: | ||
| return False | ||
| finally: | ||
| QApplication.setOverrideCursor(Qt.WaitCursor) | ||
|
|
||
| if rule_action == "delete": | ||
| self.aboutToChange() | ||
| self.database().connector.deleteTableRule(rule_name, (self.schemaName(), self.name)) | ||
| self.refreshRules() | ||
| return True | ||
|
|
||
| return Table.runAction(self, action) | ||
|
|
||
| def tableFieldsFactory(self, row, table): | ||
| return PGTableField(row, table) | ||
|
|
||
| def tableConstraintsFactory(self, row, table): | ||
| return PGTableConstraint(row, table) | ||
|
|
||
| def tableIndexesFactory(self, row, table): | ||
| return PGTableIndex(row, table) | ||
|
|
||
| def tableTriggersFactory(self, row, table): | ||
| return PGTableTrigger(row, table) | ||
|
|
||
| def tableRulesFactory(self, row, table): | ||
| return PGTableRule(row, table) | ||
|
|
||
|
|
||
| def info(self): | ||
| from .info_model import PGTableInfo | ||
| return PGTableInfo(self) | ||
|
|
||
| def tableDataModel(self, parent): | ||
| from .data_model import PGTableDataModel | ||
| return PGTableDataModel(self, parent) | ||
|
|
||
|
|
||
| class PGVectorTable(PGTable, VectorTable): | ||
| def __init__(self, row, db, schema=None): | ||
| PGTable.__init__(self, row[:-4], db, schema) | ||
| VectorTable.__init__(self, db, schema) | ||
| self.geomColumn, self.geomType, self.geomDim, self.srid = row[-4:] | ||
|
|
||
| def info(self): | ||
| from .info_model import PGVectorTableInfo | ||
| return PGVectorTableInfo(self) | ||
|
|
||
| def runAction(self, action): | ||
| if PGTable.runAction(self, action): | ||
| return True | ||
| return VectorTable.runAction(self, action) | ||
|
|
||
| class PGRasterTable(PGTable, RasterTable): | ||
| def __init__(self, row, db, schema=None): | ||
| PGTable.__init__(self, row[:-6], db, schema) | ||
| RasterTable.__init__(self, db, schema) | ||
| self.geomColumn, self.pixelType, self.pixelSizeX, self.pixelSizeY, self.isExternal, self.srid = row[-6:] | ||
| self.geomType = 'RASTER' | ||
|
|
||
| def info(self): | ||
| from .info_model import PGRasterTableInfo | ||
| return PGRasterTableInfo(self) | ||
|
|
||
| def gdalUri(self): | ||
| uri = self.database().uri() | ||
| schema = ( u'schema=%s' % self.schemaName() ) if self.schemaName() else '' | ||
| gdalUri = u'PG: dbname=%s host=%s user=%s password=%s port=%s mode=2 %s table=%s' % (uri.database(), uri.host(), uri.username(), uri.password(), uri.port(), schema, self.name) | ||
| return QString( gdalUri ) | ||
|
|
||
| def mimeUri(self): | ||
| uri = u"raster:gdal:%s:%s" % (self.name, self.gdalUri()) | ||
| return QString( uri ) | ||
|
|
||
| def toMapLayer(self): | ||
| from qgis.core import QgsRasterLayer | ||
| rl = QgsRasterLayer(self.gdalUri(), self.name) | ||
| if rl.isValid(): | ||
| rl.setContrastEnhancementAlgorithm("StretchToMinimumMaximum") | ||
| return rl | ||
|
|
||
| class PGTableField(TableField): | ||
| def __init__(self, row, table): | ||
| TableField.__init__(self, table) | ||
| self.num, self.name, self.dataType, self.charMaxLen, self.modifier, self.notNull, self.hasDefault, self.default, typeStr = row | ||
| self.primaryKey = False | ||
|
|
||
| # convert the modifier to string (e.g. "precision,scale") | ||
| if self.modifier != None and self.modifier != -1: | ||
| trimmedTypeStr = QString(typeStr).trimmed() | ||
| if trimmedTypeStr.startsWith(self.dataType): | ||
| regex = QRegExp( "%s\s*\((.+)\)$" % QRegExp.escape(self.dataType) ) | ||
| startpos = regex.indexIn( trimmedTypeStr ) | ||
| if startpos >= 0: | ||
| self.modifier = regex.cap(1).trimmed() | ||
| else: | ||
| self.modifier = None | ||
|
|
||
| # find out whether fields are part of primary key | ||
| for con in self.table().constraints(): | ||
| if con.type == TableConstraint.TypePrimaryKey and self.num in con.columns: | ||
| self.primaryKey = True | ||
| break | ||
|
|
||
|
|
||
| class PGTableConstraint(TableConstraint): | ||
| def __init__(self, row, table): | ||
| TableConstraint.__init__(self, table) | ||
| self.name, constr_type, self.isDefferable, self.isDeffered, columns = row[:5] | ||
| self.columns = map(int, columns.split(' ')) | ||
| self.type = TableConstraint.types[constr_type] # convert to enum | ||
|
|
||
| if self.type == TableConstraint.TypeCheck: | ||
| self.checkSource = row[5] | ||
| elif self.type == TableConstraint.TypeForeignKey: | ||
| self.foreignTable = row[6] | ||
| self.foreignOnUpdate = TableConstraint.onAction[row[7]] | ||
| self.foreignOnDelete = TableConstraint.onAction[row[8]] | ||
| self.foreignMatchType = TableConstraint.matchTypes[row[9]] | ||
| self.foreignKeys = row[10] | ||
|
|
||
|
|
||
| class PGTableIndex(TableIndex): | ||
| def __init__(self, row, table): | ||
| TableIndex.__init__(self, table) | ||
| self.name, columns, self.isUnique = row | ||
| self.columns = map(int, columns.split(' ')) | ||
|
|
||
|
|
||
| class PGTableTrigger(TableTrigger): | ||
| def __init__(self, row, table): | ||
| TableTrigger.__init__(self, table) | ||
| self.name, self.function, self.type, self.enabled = row | ||
|
|
||
| class PGTableRule(TableRule): | ||
| def __init__(self, row, table): | ||
| TableRule.__init__(self, table) | ||
| self.name, self.definition = row | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| INSTALL(FILES __init__.py DESTINATION ${DB_MANAGER_POSTGIS_DIR}/plugins) | ||
|
|
||
| ADD_SUBDIRECTORY(qgis_topoview) | ||
| ADD_SUBDIRECTORY(versioning) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : DB Manager | ||
| Description : Database manager plugin for QuantumGIS | ||
| Date : May 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| import os | ||
| current_dir = os.path.dirname(__file__) | ||
|
|
||
| def load(dbplugin, mainwindow): | ||
| for name in os.listdir(current_dir): | ||
| if not os.path.isdir( os.path.join( current_dir, name ) ): | ||
| continue | ||
| try: | ||
| exec( u"from .%s import load" % name ) | ||
| except ImportError, e: | ||
| continue | ||
|
|
||
| load(dbplugin, mainwindow) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| SET (DB_MANAGER_POSTGIS_TOPOVIEW_DIR ${DB_MANAGER_POSTGIS_DIR}/plugins/qgis_topoview) | ||
|
|
||
| FILE(GLOB PY_FILES *.py) | ||
|
|
||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_POSTGIS_TOPOVIEW_DIR}) | ||
| INSTALL(FILES topoview_template.qgs DESTINATION ${DB_MANAGER_POSTGIS_TOPOVIEW_DIR}) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : TopoViewer plugin for DB Manager | ||
| Description : Create a project to display topology schema on QGis | ||
| Date : Sep 23, 2011 | ||
| copyright : (C) 2011 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| Based on qgis_pgis_topoview by Sandro Santilli <strk@keybit.net> | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| import os | ||
| current_path = os.path.dirname(__file__) | ||
|
|
||
|
|
||
| # The load function is called when the "db" database or either one of its | ||
| # children db objects (table o schema) is selected by the user. | ||
| # @param db is the selected database | ||
| # @param mainwindow is the DBManager mainwindow | ||
| def load(db, mainwindow): | ||
| # check whether the selected database has topology enabled | ||
| # (search for topology.topology) | ||
| sql = u"""SELECT count(*) | ||
| FROM pg_class AS cls JOIN pg_namespace AS nsp ON nsp.oid = cls.relnamespace | ||
| WHERE cls.relname = 'topology' AND nsp.nspname = 'topology'""" | ||
| c = db.connector._get_cursor() | ||
| db.connector._execute( c, sql ) | ||
| res = db.connector._fetchone( c ) | ||
| if res == None or int(res[0]) <= 0: | ||
| return | ||
|
|
||
| # add the action to the DBManager menu | ||
| action = QAction( QIcon(), "&TopoViewer", db ) | ||
| mainwindow.registerAction( action, "&Schema", run ) | ||
|
|
||
|
|
||
| # The run function is called once the user clicks on the action TopoViewer | ||
| # (look above at the load function) from the DBManager menu/toolbar. | ||
| # @param item is the selected db item (either db, schema or table) | ||
| # @param action is the clicked action on the DBManager menu/toolbar | ||
| # @param mainwindow is the DBManager mainwindow | ||
| def run(item, action, mainwindow): | ||
| db = item.database() | ||
| uri = db.uri() | ||
| conninfo = uri.connectionInfo() | ||
|
|
||
| # check if the selected item is a topology schema | ||
| isTopoSchema = False | ||
| if hasattr(item, 'schema') and item.schema() != None: | ||
| sql = u"SELECT count(*) FROM topology.topology WHERE name = '%s'" % item.schema().name | ||
| c = db.connector._get_cursor() | ||
| db.connector._execute( c, sql ) | ||
| res = db.connector._fetchone( c ) | ||
| isTopoSchema = res != None and int(res[0]) > 0 | ||
|
|
||
| if not isTopoSchema: | ||
| QMessageBox.critical(mainwindow, "Invalid topology", u'Schema "%s" is not registered in topology.topology.' % item.schema().name) | ||
| return False | ||
|
|
||
| # create the new project from the template one | ||
| tpl_name = u'topoview_template.qgs' | ||
| toponame = item.schema().name | ||
| project_name = u'topoview_%s_%s.qgs' % (uri.database(), toponame) | ||
|
|
||
| template_file = os.path.join(current_path, tpl_name) | ||
| inf = QFile( template_file ) | ||
| if not inf.exists(): | ||
| QMessageBox.critical(mainwindow, "Error", u'Template "%s" not found!' % template_file) | ||
| return False | ||
|
|
||
| project_file = os.path.join(current_path, project_name) | ||
| outf = QFile( project_file ) | ||
| if not outf.open( QIODevice.WriteOnly ): | ||
| QMessageBox.critical(mainwindow, "Error", u'Unable to open "%s"' % project_file) | ||
| return False | ||
|
|
||
| if not inf.open( QIODevice.ReadOnly ): | ||
| QMessageBox.critical(mainwindow, "Error", u'Unable to open "%s"' % template_file) | ||
| return False | ||
|
|
||
| while not inf.atEnd(): | ||
| l = inf.readLine() | ||
| l = l.replace( u"dbname='@@DBNAME@@'", conninfo.toUtf8() ) | ||
| l = l.replace( u'@@TOPONAME@@', toponame ) | ||
| outf.write( l ) | ||
|
|
||
| inf.close() | ||
| outf.close() | ||
|
|
||
| # load the project on QGis canvas | ||
| iface = mainwindow.iface | ||
| iface.newProject( True ) | ||
| if iface.mapCanvas().layerCount() == 0: | ||
| iface.addProject( project_file ) | ||
| return True | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| SET (DB_MANAGER_POSTGIS_VERSIONING_DIR ${DB_MANAGER_POSTGIS_DIR}/plugins/versioning) | ||
|
|
||
| FILE(GLOB PY_FILES *.py) | ||
|
|
||
| FILE(GLOB UI_FILES *.ui) | ||
| PYQT4_WRAP_UI(PYUI_FILES ${UI_FILES}) | ||
| ADD_CUSTOM_TARGET(db_manager_postgis_versioning ALL DEPENDS ${PYUI_FILES}) | ||
|
|
||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_POSTGIS_VERSIONING_DIR}) | ||
| INSTALL(FILES ${PYUI_FILES} DESTINATION ${DB_MANAGER_POSTGIS_VERSIONING_DIR}) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <ui version="4.0"> | ||
| <class>DlgVersioning</class> | ||
| <widget class="QDialog" name="DlgVersioning"> | ||
| <property name="geometry"> | ||
| <rect> | ||
| <x>0</x> | ||
| <y>0</y> | ||
| <width>774</width> | ||
| <height>395</height> | ||
| </rect> | ||
| </property> | ||
| <property name="windowTitle"> | ||
| <string>Add versioning support to a table</string> | ||
| </property> | ||
| <layout class="QGridLayout" name="gridLayout_3"> | ||
| <item row="0" column="0" rowspan="2"> | ||
| <layout class="QVBoxLayout" name="verticalLayout"> | ||
| <item> | ||
| <widget class="QLabel" name="label_4"> | ||
| <property name="text"> | ||
| <string>Table is expected to be empty, with a primary key.</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item> | ||
| <layout class="QGridLayout" name="gridLayout"> | ||
| <item row="0" column="0"> | ||
| <widget class="QLabel" name="label_2"> | ||
| <property name="text"> | ||
| <string>Schema</string> | ||
| </property> | ||
| <property name="alignment"> | ||
| <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="0" column="1"> | ||
| <widget class="QComboBox" name="cboSchema"/> | ||
| </item> | ||
| <item row="1" column="0"> | ||
| <widget class="QLabel" name="label_3"> | ||
| <property name="text"> | ||
| <string>Table</string> | ||
| </property> | ||
| <property name="alignment"> | ||
| <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="1" column="1"> | ||
| <widget class="QComboBox" name="cboTable"/> | ||
| </item> | ||
| <item row="0" column="2" rowspan="2"> | ||
| <spacer name="horizontalSpacer"> | ||
| <property name="orientation"> | ||
| <enum>Qt::Horizontal</enum> | ||
| </property> | ||
| <property name="sizeType"> | ||
| <enum>QSizePolicy::Preferred</enum> | ||
| </property> | ||
| <property name="sizeHint" stdset="0"> | ||
| <size> | ||
| <width>40</width> | ||
| <height>48</height> | ||
| </size> | ||
| </property> | ||
| </spacer> | ||
| </item> | ||
| </layout> | ||
| </item> | ||
| <item> | ||
| <widget class="QCheckBox" name="chkCreateCurrent"> | ||
| <property name="text"> | ||
| <string>create a view with current content (<TABLE>_current)</string> | ||
| </property> | ||
| <property name="checked"> | ||
| <bool>true</bool> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item> | ||
| <widget class="QGroupBox" name="groupBox_2"> | ||
| <property name="title"> | ||
| <string>New columns</string> | ||
| </property> | ||
| <layout class="QGridLayout" name="gridLayout_2"> | ||
| <item row="0" column="0"> | ||
| <widget class="QLabel" name="label_6"> | ||
| <property name="text"> | ||
| <string>Prim. key</string> | ||
| </property> | ||
| <property name="alignment"> | ||
| <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="0" column="1"> | ||
| <widget class="QLineEdit" name="editPkey"> | ||
| <property name="text"> | ||
| <string>id_hist</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="1" column="0"> | ||
| <widget class="QLabel" name="label_7"> | ||
| <property name="text"> | ||
| <string>Start time</string> | ||
| </property> | ||
| <property name="alignment"> | ||
| <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="1" column="1"> | ||
| <widget class="QLineEdit" name="editStart"> | ||
| <property name="text"> | ||
| <string>time_start</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="2" column="0"> | ||
| <widget class="QLabel" name="label_8"> | ||
| <property name="text"> | ||
| <string>End time</string> | ||
| </property> | ||
| <property name="alignment"> | ||
| <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="2" column="1"> | ||
| <widget class="QLineEdit" name="editEnd"> | ||
| <property name="text"> | ||
| <string>time_end</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| </layout> | ||
| </widget> | ||
| </item> | ||
| <item> | ||
| <spacer name="verticalSpacer"> | ||
| <property name="orientation"> | ||
| <enum>Qt::Vertical</enum> | ||
| </property> | ||
| <property name="sizeHint" stdset="0"> | ||
| <size> | ||
| <width>20</width> | ||
| <height>40</height> | ||
| </size> | ||
| </property> | ||
| </spacer> | ||
| </item> | ||
| </layout> | ||
| </item> | ||
| <item row="0" column="1"> | ||
| <widget class="QLabel" name="label_5"> | ||
| <property name="text"> | ||
| <string>SQL to be executed:</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="1" column="1"> | ||
| <widget class="QTextBrowser" name="txtSql"/> | ||
| </item> | ||
| <item row="2" column="0" colspan="2"> | ||
| <widget class="QDialogButtonBox" name="buttonBox"> | ||
| <property name="orientation"> | ||
| <enum>Qt::Horizontal</enum> | ||
| </property> | ||
| <property name="standardButtons"> | ||
| <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| </layout> | ||
| </widget> | ||
| <tabstops> | ||
| <tabstop>cboSchema</tabstop> | ||
| <tabstop>cboTable</tabstop> | ||
| <tabstop>chkCreateCurrent</tabstop> | ||
| <tabstop>editPkey</tabstop> | ||
| <tabstop>editStart</tabstop> | ||
| <tabstop>editEnd</tabstop> | ||
| <tabstop>txtSql</tabstop> | ||
| <tabstop>buttonBox</tabstop> | ||
| </tabstops> | ||
| <resources/> | ||
| <connections> | ||
| <connection> | ||
| <sender>buttonBox</sender> | ||
| <signal>rejected()</signal> | ||
| <receiver>DlgVersioning</receiver> | ||
| <slot>reject()</slot> | ||
| <hints> | ||
| <hint type="sourcelabel"> | ||
| <x>335</x> | ||
| <y>465</y> | ||
| </hint> | ||
| <hint type="destinationlabel"> | ||
| <x>286</x> | ||
| <y>274</y> | ||
| </hint> | ||
| </hints> | ||
| </connection> | ||
| </connections> | ||
| </ui> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| # Form implementation generated from reading ui file 'ui/DlgVersioning.ui' | ||
| # | ||
| # Created: Tue Nov 15 13:58:12 2011 | ||
| # by: PyQt4 UI code generator 4.8.3 | ||
| # | ||
| # WARNING! All changes made in this file will be lost! | ||
|
|
||
| from PyQt4 import QtCore, QtGui | ||
|
|
||
| try: | ||
| _fromUtf8 = QtCore.QString.fromUtf8 | ||
| except AttributeError: | ||
| _fromUtf8 = lambda s: s | ||
|
|
||
| class Ui_DlgVersioning(object): | ||
| def setupUi(self, DlgVersioning): | ||
| DlgVersioning.setObjectName(_fromUtf8("DlgVersioning")) | ||
| DlgVersioning.resize(774, 395) | ||
| self.gridLayout_3 = QtGui.QGridLayout(DlgVersioning) | ||
| self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) | ||
| self.verticalLayout = QtGui.QVBoxLayout() | ||
| self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) | ||
| self.label_4 = QtGui.QLabel(DlgVersioning) | ||
| self.label_4.setObjectName(_fromUtf8("label_4")) | ||
| self.verticalLayout.addWidget(self.label_4) | ||
| self.gridLayout = QtGui.QGridLayout() | ||
| self.gridLayout.setObjectName(_fromUtf8("gridLayout")) | ||
| self.label_2 = QtGui.QLabel(DlgVersioning) | ||
| self.label_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) | ||
| self.label_2.setObjectName(_fromUtf8("label_2")) | ||
| self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1) | ||
| self.cboSchema = QtGui.QComboBox(DlgVersioning) | ||
| self.cboSchema.setObjectName(_fromUtf8("cboSchema")) | ||
| self.gridLayout.addWidget(self.cboSchema, 0, 1, 1, 1) | ||
| self.label_3 = QtGui.QLabel(DlgVersioning) | ||
| self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) | ||
| self.label_3.setObjectName(_fromUtf8("label_3")) | ||
| self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) | ||
| self.cboTable = QtGui.QComboBox(DlgVersioning) | ||
| self.cboTable.setObjectName(_fromUtf8("cboTable")) | ||
| self.gridLayout.addWidget(self.cboTable, 1, 1, 1, 1) | ||
| spacerItem = QtGui.QSpacerItem(40, 48, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) | ||
| self.gridLayout.addItem(spacerItem, 0, 2, 2, 1) | ||
| self.verticalLayout.addLayout(self.gridLayout) | ||
| self.chkCreateCurrent = QtGui.QCheckBox(DlgVersioning) | ||
| self.chkCreateCurrent.setChecked(True) | ||
| self.chkCreateCurrent.setObjectName(_fromUtf8("chkCreateCurrent")) | ||
| self.verticalLayout.addWidget(self.chkCreateCurrent) | ||
| self.groupBox_2 = QtGui.QGroupBox(DlgVersioning) | ||
| self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) | ||
| self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) | ||
| self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) | ||
| self.label_6 = QtGui.QLabel(self.groupBox_2) | ||
| self.label_6.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) | ||
| self.label_6.setObjectName(_fromUtf8("label_6")) | ||
| self.gridLayout_2.addWidget(self.label_6, 0, 0, 1, 1) | ||
| self.editPkey = QtGui.QLineEdit(self.groupBox_2) | ||
| self.editPkey.setObjectName(_fromUtf8("editPkey")) | ||
| self.gridLayout_2.addWidget(self.editPkey, 0, 1, 1, 1) | ||
| self.label_7 = QtGui.QLabel(self.groupBox_2) | ||
| self.label_7.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) | ||
| self.label_7.setObjectName(_fromUtf8("label_7")) | ||
| self.gridLayout_2.addWidget(self.label_7, 1, 0, 1, 1) | ||
| self.editStart = QtGui.QLineEdit(self.groupBox_2) | ||
| self.editStart.setObjectName(_fromUtf8("editStart")) | ||
| self.gridLayout_2.addWidget(self.editStart, 1, 1, 1, 1) | ||
| self.label_8 = QtGui.QLabel(self.groupBox_2) | ||
| self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) | ||
| self.label_8.setObjectName(_fromUtf8("label_8")) | ||
| self.gridLayout_2.addWidget(self.label_8, 2, 0, 1, 1) | ||
| self.editEnd = QtGui.QLineEdit(self.groupBox_2) | ||
| self.editEnd.setObjectName(_fromUtf8("editEnd")) | ||
| self.gridLayout_2.addWidget(self.editEnd, 2, 1, 1, 1) | ||
| self.verticalLayout.addWidget(self.groupBox_2) | ||
| spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) | ||
| self.verticalLayout.addItem(spacerItem1) | ||
| self.gridLayout_3.addLayout(self.verticalLayout, 0, 0, 2, 1) | ||
| self.label_5 = QtGui.QLabel(DlgVersioning) | ||
| self.label_5.setObjectName(_fromUtf8("label_5")) | ||
| self.gridLayout_3.addWidget(self.label_5, 0, 1, 1, 1) | ||
| self.txtSql = QtGui.QTextBrowser(DlgVersioning) | ||
| self.txtSql.setObjectName(_fromUtf8("txtSql")) | ||
| self.gridLayout_3.addWidget(self.txtSql, 1, 1, 1, 1) | ||
| self.buttonBox = QtGui.QDialogButtonBox(DlgVersioning) | ||
| self.buttonBox.setOrientation(QtCore.Qt.Horizontal) | ||
| self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Help|QtGui.QDialogButtonBox.Ok) | ||
| self.buttonBox.setObjectName(_fromUtf8("buttonBox")) | ||
| self.gridLayout_3.addWidget(self.buttonBox, 2, 0, 1, 2) | ||
|
|
||
| self.retranslateUi(DlgVersioning) | ||
| QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), DlgVersioning.reject) | ||
| QtCore.QMetaObject.connectSlotsByName(DlgVersioning) | ||
| DlgVersioning.setTabOrder(self.cboSchema, self.cboTable) | ||
| DlgVersioning.setTabOrder(self.cboTable, self.chkCreateCurrent) | ||
| DlgVersioning.setTabOrder(self.chkCreateCurrent, self.editPkey) | ||
| DlgVersioning.setTabOrder(self.editPkey, self.editStart) | ||
| DlgVersioning.setTabOrder(self.editStart, self.editEnd) | ||
| DlgVersioning.setTabOrder(self.editEnd, self.txtSql) | ||
| DlgVersioning.setTabOrder(self.txtSql, self.buttonBox) | ||
|
|
||
| def retranslateUi(self, DlgVersioning): | ||
| DlgVersioning.setWindowTitle(QtGui.QApplication.translate("DlgVersioning", "Add versioning support to a table", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_4.setText(QtGui.QApplication.translate("DlgVersioning", "Table is expected to be empty, with a primary key.", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_2.setText(QtGui.QApplication.translate("DlgVersioning", "Schema", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_3.setText(QtGui.QApplication.translate("DlgVersioning", "Table", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.chkCreateCurrent.setText(QtGui.QApplication.translate("DlgVersioning", "create a view with current content (<TABLE>_current)", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.groupBox_2.setTitle(QtGui.QApplication.translate("DlgVersioning", "New columns", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_6.setText(QtGui.QApplication.translate("DlgVersioning", "Prim. key", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.editPkey.setText(QtGui.QApplication.translate("DlgVersioning", "id_hist", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_7.setText(QtGui.QApplication.translate("DlgVersioning", "Start time", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.editStart.setText(QtGui.QApplication.translate("DlgVersioning", "time_start", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_8.setText(QtGui.QApplication.translate("DlgVersioning", "End time", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.editEnd.setText(QtGui.QApplication.translate("DlgVersioning", "time_end", None, QtGui.QApplication.UnicodeUTF8)) | ||
| self.label_5.setText(QtGui.QApplication.translate("DlgVersioning", "SQL to be executed:", None, QtGui.QApplication.UnicodeUTF8)) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : Versioning plugin for DB Manager | ||
| Description : Set up versioning support for a table | ||
| Date : Mar 12, 2012 | ||
| copyright : (C) 2012 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| # The load function is called when the "db" database or either one of its | ||
| # children db objects (table o schema) is selected by the user. | ||
| # @param db is the selected database | ||
| # @param mainwindow is the DBManager mainwindow | ||
| def load(db, mainwindow): | ||
| # add the action to the DBManager menu | ||
| action = QAction( QIcon(), "&Versioning", db ) | ||
| mainwindow.registerAction( action, "&Table", run ) | ||
|
|
||
|
|
||
| # The run function is called once the user clicks on the action TopoViewer | ||
| # (look above at the load function) from the DBManager menu/toolbar. | ||
| # @param item is the selected db item (either db, schema or table) | ||
| # @param action is the clicked action on the DBManager menu/toolbar | ||
| # @param mainwindow is the DBManager mainwindow | ||
| def run(item, action, mainwindow): | ||
| from .dlg_versioning import DlgVersioning | ||
| dlg = DlgVersioning( item, mainwindow ) | ||
|
|
||
| QApplication.restoreOverrideCursor() | ||
| try: | ||
| dlg.exec_() | ||
| finally: | ||
| QApplication.setOverrideCursor(Qt.WaitCursor) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,276 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| /*************************************************************************** | ||
| Name : Versioning plugin for DB Manager | ||
| Description : Set up versioning support for a table | ||
| Date : Mar 12, 2012 | ||
| copyright : (C) 2012 by Giuseppe Sucameli | ||
| email : brush.tyler@gmail.com | ||
| Based on PG_Manager by Martin Dobias <wonder.sk@gmail.com> (GPLv2 license) | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * This program is free software; you can redistribute it and/or modify * | ||
| * it under the terms of the GNU General Public License as published by * | ||
| * the Free Software Foundation; either version 2 of the License, or * | ||
| * (at your option) any later version. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| from PyQt4.QtCore import * | ||
| from PyQt4.QtGui import * | ||
|
|
||
| from ui_DlgVersioning import Ui_DlgVersioning | ||
|
|
||
| from .....dlg_db_error import DlgDbError | ||
| from ....plugin import BaseError, Table | ||
|
|
||
| class DlgVersioning(QDialog, Ui_DlgVersioning): | ||
|
|
||
| def __init__(self, item, parent=None): | ||
| QDialog.__init__(self, parent) | ||
| self.item = item | ||
| self.setupUi(self) | ||
|
|
||
| self.db = self.item.database() | ||
| self.schemas = self.db.schemas() | ||
| self.hasSchemas = self.schemas != None | ||
|
|
||
| self.connect(self.buttonBox, SIGNAL("accepted()"), self.onOK) | ||
| self.connect(self.buttonBox, SIGNAL("helpRequested()"), self.showHelp) | ||
|
|
||
| self.populateSchemas() | ||
| self.populateTables() | ||
|
|
||
| if isinstance(item, Table): | ||
| index = self.cboTable.findText(self.item.name) | ||
| if index >= 0: | ||
| self.cboTable.setCurrentIndex(index) | ||
|
|
||
| self.connect(self.cboSchema, SIGNAL("currentIndexChanged(int)"), self.populateTables) | ||
|
|
||
| # updates of SQL window | ||
| self.connect(self.cboSchema, SIGNAL("currentIndexChanged(int)"), self.updateSql) | ||
| self.connect(self.cboTable, SIGNAL("currentIndexChanged(int)"), self.updateSql) | ||
| self.connect(self.chkCreateCurrent, SIGNAL("stateChanged(int)"), self.updateSql) | ||
| self.connect(self.editPkey, SIGNAL("textChanged(const QString &)"), self.updateSql) | ||
| self.connect(self.editStart, SIGNAL("textChanged(const QString &)"), self.updateSql) | ||
| self.connect(self.editEnd, SIGNAL("textChanged(const QString &)"), self.updateSql) | ||
|
|
||
| self.updateSql() | ||
|
|
||
|
|
||
| def populateSchemas(self): | ||
| self.cboSchema.clear() | ||
| if not self.hasSchemas: | ||
| self.hideSchemas() | ||
| return | ||
|
|
||
| index = -1 | ||
| for schema in self.schemas: | ||
| self.cboSchema.addItem(schema.name) | ||
| if hasattr(self.item, 'schema') and schema.name == self.item.schema().name: | ||
| index = self.cboSchema.count()-1 | ||
| self.cboSchema.setCurrentIndex(index) | ||
|
|
||
| def hideSchemas(self): | ||
| self.cboSchema.setEnabled(False) | ||
|
|
||
| def populateTables(self): | ||
| self.tables = [] | ||
|
|
||
| schemas = self.db.schemas() | ||
| if schemas != None: | ||
| schema_name = self.cboSchema.currentText() | ||
| matching_schemas = filter(lambda x: x.name == schema_name, schemas) | ||
| tables = matching_schemas[0].tables() if len(matching_schemas) > 0 else [] | ||
| else: | ||
| tables = self.db.tables() | ||
|
|
||
| self.cboTable.clear() | ||
| for table in tables: | ||
| if table.type == table.VectorType: # contains geometry column? | ||
| self.tables.append( table ) | ||
| self.cboTable.addItem(table.name) | ||
|
|
||
|
|
||
| def get_escaped_name(self, schema, table, suffix): | ||
| name = self.db.connector.quoteId( u"%s%s" % (table, suffix) ) | ||
| schema_name = self.db.connector.quoteId(schema) if schema else None | ||
| return u"%s.%s" % (schema_name, name) if schema_name else name | ||
|
|
||
| def updateSql(self): | ||
| if self.cboTable.currentIndex() < 0 or len(self.tables) < self.cboTable.currentIndex(): | ||
| return | ||
|
|
||
| self.table = self.tables[ self.cboTable.currentIndex() ] | ||
| self.schematable = self.table.quotedName() | ||
|
|
||
| self.current = self.chkCreateCurrent.isChecked() | ||
|
|
||
| self.colPkey = self.db.connector.quoteId(self.editPkey.text()) | ||
| self.colStart = self.db.connector.quoteId(self.editStart.text()) | ||
| self.colEnd = self.db.connector.quoteId(self.editEnd.text()) | ||
|
|
||
| self.columns = map(lambda x: self.db.connector.quoteId(x.name), self.table.fields()) | ||
|
|
||
| self.colOrigPkey = None | ||
| for constr in self.table.constraints(): | ||
| if constr.type == constr.TypePrimaryKey: | ||
| self.origPkeyName = self.db.connector.quoteId(constr.name) | ||
| self.colOrigPkey = map(lambda (x, y): self.db.connector.quoteId(y.name), constr.fields().iteritems()) | ||
| break | ||
|
|
||
| if self.colOrigPkey is None: | ||
| self.txtSql.setPlainText("Table doesn't have a primary key!") | ||
| self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) | ||
| return | ||
| elif len(self.colOrigPkey) > 1: | ||
| self.txtSql.setPlainText("Table has multicolumn primary key!") | ||
| self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) | ||
| return | ||
|
|
||
| # take first (and only column of the pkey) | ||
| self.colOrigPkey = self.colOrigPkey[0] | ||
|
|
||
| # define view, function, rule and trigger names | ||
| self.view = self.get_escaped_name( self.table.schemaName(), self.table.name, "_current" ) | ||
|
|
||
| self.func_at_time = self.get_escaped_name( self.table.schemaName(), self.table.name, "_at_time" ) | ||
| self.func_update = self.get_escaped_name( self.table.schemaName(), self.table.name, "_update" ) | ||
| self.func_insert = self.get_escaped_name( self.table.schemaName(), self.table.name, "_insert" ) | ||
|
|
||
| self.rule_del = self.get_escaped_name( None, self.table.name, "_del" ) | ||
| self.trigger_update = self.get_escaped_name( None, self.table.name, "_update" ) | ||
| self.trigger_insert = self.get_escaped_name( None, self.table.name, "_insert" ) | ||
|
|
||
|
|
||
| sql = [] | ||
|
|
||
| # modify table: add serial column, start time, end time | ||
| sql.append( self.sql_alterTable() ) | ||
| # add primary key to the table | ||
| sql.append( self.sql_setPkey() ) | ||
|
|
||
| sql.append( self.sql_currentView() ) | ||
| # add X_at_time, X_update, X_delete functions | ||
| sql.append( self.sql_functions() ) | ||
| # add insert, update trigger, delete rule | ||
| sql.append( self.sql_triggers() ) | ||
| # add _current view + updatable | ||
| #if self.current: | ||
| sql.append( self.sql_updatesView() ) | ||
|
|
||
| self.txtSql.setPlainText( u'\n\n'.join(sql) ) | ||
| self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True) | ||
|
|
||
| return sql | ||
|
|
||
| def showHelp(self): | ||
| helpText = u"""In this dialog you can set up versioning support for a table. The table will be modified so that all changes will be recorded: there will be a column with start time and end time. Every row will have its start time, end time is assigned when the feature gets deleted. When a row is modified, the original data is marked with end time and new row is created. With this system, it's possible to get back to state of the table any time in history. When selecting rows from the table, you will always have to specify at what time do you want the rows.""" | ||
| QMessageBox.information(self, "Help", helpText) | ||
|
|
||
|
|
||
| def sql_alterTable(self): | ||
| return u"ALTER TABLE %s ADD %s serial, ADD %s timestamp, ADD %s timestamp;" % (self.schematable, self.colPkey, self.colStart, self.colEnd) | ||
|
|
||
| def sql_setPkey(self): | ||
| return u"ALTER TABLE %s DROP CONSTRAINT %s, ADD PRIMARY KEY (%s);" % (self.schematable, self.origPkeyName, self.colPkey) | ||
|
|
||
| def sql_currentView(self): | ||
| cols = ",".join(self.columns) | ||
|
|
||
| return u"CREATE VIEW %(view)s AS SELECT %(cols)s FROM %(schematable)s WHERE %(end)s IS NULL;" % \ | ||
| { 'view' : self.view, 'cols' : cols, 'schematable' : self.schematable, 'end' : self.colEnd } | ||
|
|
||
|
|
||
| def sql_functions(self): | ||
| cols = ",".join(self.columns) | ||
| old_cols = ",".join(map(lambda x: u"OLD." + x, self.columns)) | ||
|
|
||
| sql = u""" | ||
| CREATE OR REPLACE FUNCTION %(func_at_time)s(timestamp) | ||
| RETURNS SETOF %(view)s AS | ||
| $$ | ||
| SELECT %(cols)s FROM %(schematable)s WHERE | ||
| ( SELECT CASE WHEN %(end)s IS NULL THEN (%(start)s <= $1) ELSE (%(start)s <= $1 AND %(end)s > $1) END ); | ||
| $$ | ||
| LANGUAGE 'SQL'; | ||
| CREATE OR REPLACE FUNCTION %(func_update)s() | ||
| RETURNS TRIGGER AS | ||
| $$ | ||
| BEGIN | ||
| IF OLD.%(end)s IS NOT NULL THEN | ||
| RETURN NULL; | ||
| END IF; | ||
| IF NEW.%(end)s IS NULL THEN | ||
| INSERT INTO %(schematable)s (%(cols)s, %(start)s, %(end)s) VALUES (%(oldcols)s, OLD.%(start)s, current_timestamp); | ||
| NEW.%(start)s = current_timestamp; | ||
| END IF; | ||
| RETURN NEW; | ||
| END; | ||
| $$ | ||
| LANGUAGE 'plpgsql'; | ||
| CREATE OR REPLACE FUNCTION %(func_insert)s() | ||
| RETURNS trigger AS | ||
| $$ | ||
| BEGIN | ||
| if NEW.%(start)s IS NULL then | ||
| NEW.%(start)s = now(); | ||
| NEW.%(end)s = null; | ||
| end if; | ||
| RETURN NEW; | ||
| END; | ||
| $$ | ||
| LANGUAGE 'plpgsql';""" % { 'view' : self.view, 'schematable': self.schematable, 'cols' : cols, 'oldcols' : old_cols, 'start' : self.colStart, 'end' : self.colEnd, 'func_at_time' : self.func_at_time, 'func_update' : self.func_update, 'func_insert' : self.func_insert } | ||
| return sql | ||
|
|
||
| def sql_triggers(self): | ||
| return u""" | ||
| CREATE RULE %(rule_del)s AS ON DELETE TO %(schematable)s | ||
| DO INSTEAD UPDATE %(schematable)s SET %(end)s = current_timestamp WHERE %(pkey)s = OLD.%(pkey)s AND %(end)s IS NULL; | ||
| CREATE TRIGGER %(trigger_update)s BEFORE UPDATE ON %(schematable)s | ||
| FOR EACH ROW EXECUTE PROCEDURE %(func_update)s(); | ||
| CREATE TRIGGER %(trigger_insert)s BEFORE INSERT ON %(schematable)s | ||
| FOR EACH ROW EXECUTE PROCEDURE %(func_insert)s();""" % \ | ||
| { 'rule_del' : self.rule_del, 'trigger_update' : self.trigger_update, 'trigger_insert' : self.trigger_insert, 'func_update' : self.func_update, 'func_insert' : self.func_insert, 'schematable' : self.schematable, 'pkey' : self.colPkey, 'end' : self.colEnd } | ||
|
|
||
| def sql_updatesView(self): | ||
| cols = ",".join(self.columns) | ||
| new_cols = ",".join(map(lambda x: u"NEW." + x, self.columns)) | ||
| assign_cols = ",".join(map(lambda x: u"%s = NEW.%s" % (x,x), self.columns)) | ||
|
|
||
| return u""" | ||
| CREATE OR REPLACE RULE "_DELETE" AS ON DELETE TO %(view)s DO INSTEAD | ||
| DELETE FROM %(schematable)s WHERE %(origpkey)s = old.%(origpkey)s; | ||
| CREATE OR REPLACE RULE "_INSERT" AS ON INSERT TO %(view)s DO INSTEAD | ||
| INSERT INTO %(schematable)s (%(cols)s) VALUES (%(newcols)s); | ||
| CREATE OR REPLACE RULE "_UPDATE" AS ON UPDATE TO %(view)s DO INSTEAD | ||
| UPDATE %(schematable)s SET %(assign)s WHERE %(origpkey)s = NEW.%(origpkey)s;""" % { 'view': self.view, 'schematable' : self.schematable, 'cols' : cols, 'newcols' : new_cols, 'assign' : assign_cols, 'origpkey' : self.colOrigPkey } | ||
|
|
||
|
|
||
| def onOK(self): | ||
| # execute and commit the code | ||
| QApplication.setOverrideCursor(Qt.WaitCursor) | ||
| try: | ||
| sql = u"\n".join(self.updateSql()) | ||
| self.db.connector._execute_and_commit( sql ) | ||
|
|
||
| except BaseError, e: | ||
| DlgDbError.showError(e, self) | ||
| return | ||
|
|
||
| finally: | ||
| QApplication.restoreOverrideCursor() | ||
|
|
||
| QMessageBox.information(self, "good!", "everything went fine!") | ||
| self.accept() | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <RCC> | ||
| <qresource prefix="/db_manager/postgis"> | ||
| <file alias="icon">icons/postgis_elephant.png</file> | ||
| </qresource> | ||
| </RCC> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # keywords | ||
| keywords = [ | ||
| # TODO get them from a reference page | ||
| "action", "add", "after", "all", "alter", "analyze", "and", "as", "asc", | ||
| "before", "begin", "between", "by", "cascade", "case", "cast", "check", | ||
| "collate", "column", "commit", "constraint", "create", "cross", "current_date", | ||
| "current_time", "current_timestamp", "default", "deferrable", "deferred", | ||
| "delete", "desc", "distinct", "drop", "each", "else", "end", "escape", | ||
| "except", "exists", "for", "foreign", "from", "full", "group", "having", | ||
| "ignore", "immediate", "in", "initially", "inner", "insert", "intersect", | ||
| "into", "is", "isnull", "join", "key", "left", "like", "limit", "match", | ||
| "natural", "no", "not", "notnull", "null", "of", "offset", "on", "or", "order", | ||
| "outer", "primary", "references", "release", "restrict", "right", "rollback", | ||
| "row", "savepoint", "select", "set", "table", "temporary", "then", "to", | ||
| "transaction", "trigger", "union", "unique", "update", "using", "values", | ||
| "view", "when", "where", | ||
|
|
||
| "absolute", "admin", "aggregate", "alias", "allocate", "analyse", "any", "are", | ||
| "array", "asensitive", "assertion", "asymmetric", "at", "atomic", | ||
| "authorization", "avg", "bigint", "binary", "bit", "bit_length", "blob", | ||
| "boolean", "both", "breadth", "call", "called", "cardinality", "cascaded", | ||
| "catalog", "ceil", "ceiling", "char", "character", "character_length", | ||
| "char_length", "class", "clob", "close", "coalesce", "collation", "collect", | ||
| "completion", "condition", "connect", "connection", "constraints", | ||
| "constructor", "continue", "convert", "corr", "corresponding", "count", | ||
| "covar_pop", "covar_samp", "cube", "cume_dist", "current", | ||
| "current_default_transform_group", "current_path", "current_role", | ||
| "current_transform_group_for_type", "current_user", "cursor", "cycle", "data", | ||
| "date", "day", "deallocate", "dec", "decimal", "declare", "dense_rank", | ||
| "depth", "deref", "describe", "descriptor", "destroy", "destructor", | ||
| "deterministic", "diagnostics", "dictionary", "disconnect", "do", "domain", | ||
| "double", "dynamic", "element", "end-exec", "equals", "every", "exception", | ||
| "exec", "execute", "exp", "external", "extract", "false", "fetch", "filter", | ||
| "first", "float", "floor", "found", "free", "freeze", "function", "fusion", | ||
| "general", "get", "global", "go", "goto", "grant", "grouping", "hold", "host", | ||
| "hour", "identity", "ilike", "indicator", "initialize", "inout", "input", | ||
| "insensitive", "int", "integer", "intersection", "interval", "isolation", | ||
| "iterate", "language", "large", "last", "lateral", "leading", "less", "level", | ||
| "ln", "local", "localtime", "localtimestamp", "locator", "lower", "map", "max", | ||
| "member", "merge", "method", "min", "minute", "mod", "modifies", "modify", | ||
| "module", "month", "multiset", "names", "national", "nchar", "nclob", "new", | ||
| "next", "none", "normalize", "nullif", "numeric", "object", "octet_length", | ||
| "off", "old", "only", "open", "operation", "option", "ordinality", "out", | ||
| "output", "over", "overlaps", "overlay", "pad", "parameter", "parameters", | ||
| "partial", "partition", "path", "percentile_cont", "percentile_disc", | ||
| "percent_rank", "placing", "position", "postfix", "power", "precision", | ||
| "prefix", "preorder", "prepare", "preserve", "prior", "privileges", | ||
| "procedure", "public", "range", "rank", "read", "reads", "real", "recursive", | ||
| "ref", "referencing", "regr_avgx", "regr_avgy", "regr_count", "regr_intercept", | ||
| "regr_r2", "regr_slope", "regr_sxx", "regr_sxy", "regr_syy", "relative", | ||
| "result", "return", "returning", "returns", "revoke", "role", "rollup", | ||
| "routine", "rows", "row_number", "schema", "scope", "scroll", "search", | ||
| "second", "section", "sensitive", "sequence", "session", "session_user", | ||
| "sets", "similar", "size", "smallint", "some", "space", "specific", | ||
| "specifictype", "sql", "sqlcode", "sqlerror", "sqlexception", "sqlstate", | ||
| "sqlwarning", "sqrt", "start", "state", "statement", "static", "stddev_pop", | ||
| "stddev_samp", "structure", "submultiset", "substring", "sum", "symmetric", | ||
| "system", "system_user", "tablesample", "terminate", "than", "time", | ||
| "timestamp", "timezone_hour", "timezone_minute", "trailing", "translate", | ||
| "translation", "treat", "trim", "true", "uescape", "under", "unknown", | ||
| "unnest", "upper", "usage", "user", "value", "varchar", "variable", "varying", | ||
| "var_pop", "var_samp", "verbose", "whenever", "width_bucket", "window", "with", | ||
| "within", "without", "work", "write", "xml", "xmlagg", "xmlattributes", | ||
| "xmlbinary", "xmlcomment", "xmlconcat", "xmlelement", "xmlforest", | ||
| "xmlnamespaces", "xmlparse", "xmlpi", "xmlroot", "xmlserialize", "year", "zone" | ||
| ] | ||
| postgis_keywords = [] | ||
|
|
||
| # functions | ||
| functions = [ | ||
| # TODO get them from a reference page | ||
| "abs", "changes", "coalesce", "glob", "ifnull", "hex", "last_insert_rowid", | ||
| "length", "like", "lower", "ltrim", "max", "min", "nullif", "quote", "random", | ||
| "randomblob", "replace", "round", "rtrim", "soundex", "total_change", "trim", | ||
| "typeof", "upper", "zeroblob", "date", "datetime", "julianday", "strftime", | ||
| "avg", "count", "group_concat", "sum", "total" | ||
| ] | ||
| postgis_functions = [ # from http://www.postgis.org/docs/reference.html | ||
| # 7.1. PostgreSQL PostGIS Types | ||
| "box2d", "box3d", "box3d_extent", "geometry", "geometry_dump", "geography", | ||
| # 7.2. Management Functions | ||
| "addgeometrycolumn", "dropgeometrycolumn", "dropgeometrytable", "postgis_full_version", "postgis_geos_version", "postgis_libxml_version", "postgis_lib_build_date", "postgis_lib_version", "postgis_proj_version", "postgis_scripts_build_date", "postgis_scripts_installed", "postgis_scripts_released", "postgis_uses_stats", "postgis_version", "populate_geometry_columns", "probe_geometry_columns", "updategeometrysrid", | ||
| # 7.3. Geometry Constructors | ||
| "st_bdpolyfromtext", "st_bdmpolyfromtext", "st_geogfromtext", "st_geographyfromtext", "st_geogfromwkb", "st_geomcollfromtext", "st_geomfromewkb", "st_geomfromewkt", "st_geometryfromtext", "st_geomfromgml", "st_geomfromkml", "st_gmltosql", "st_geomfromtext", "st_geomfromwkb", "st_linefrommultipoint", "st_linefromtext", "st_linefromwkb", "st_linestringfromwkb", "st_makebox2d", "st_makebox3d", "st_makeline", "st_makeenvelope", "st_makepolygon", "st_makepoint", "st_makepointm", "st_mlinefromtext", "st_mpointfromtext", "st_mpolyfromtext", "st_point", "st_pointfromtext", "st_pointfromwkb", "st_polygon", "st_polygonfromtext", "st_wkbtosql", "st_wkttosql", | ||
| # 7.4. Geometry Accessors | ||
| "geometrytype", "st_boundary", "st_coorddim", "st_dimension", "st_endpoint", "st_envelope", "st_exteriorring", "st_geometryn", "st_geometrytype", "st_interiorringn", "st_isclosed", "st_isempty", "st_isring", "st_issimple", "st_isvalid", "st_isvalidreason", "st_m", "st_ndims", "st_npoints", "st_nrings", "st_numgeometries", "st_numinteriorrings", "st_numinteriorring", "st_numpoints", "st_pointn", "st_srid", "st_startpoint", "st_summary", "st_x", "st_y", "st_z", "st_zmflag", | ||
| # 7.5. Geometry Editors | ||
| "st_addpoint", "st_affine", "st_force_2d", "st_force_3d", "st_force_3dz", "st_force_3dm", "st_force_4d", "st_force_collection", "st_forcerhr", "st_linemerge", "st_collectionextract", "st_multi", "st_removepoint", "st_reverse", "st_rotate", "st_rotatex", "st_rotatey", "st_rotatez", "st_scale", "st_segmentize", "st_setpoint", "st_setsrid", "st_snaptogrid", "st_transform", "st_translate", "st_transscale", | ||
| # 7.6. Geometry Outputs | ||
| "st_asbinary", "st_asewkb", "st_asewkt", "st_asgeojson", "st_asgml", "st_ashexewkb", "st_askml", "st_assvg", "st_geohash", "st_astext", | ||
| # 7.7. Operators | ||
| # 7.8. Spatial Relationships and Measurements | ||
| "st_area", "st_azimuth", "st_centroid", "st_closestpoint", "st_contains", "st_containsproperly", "st_covers", "st_coveredby", "st_crosses", "st_linecrossingdirection", "st_disjoint", "st_distance", "st_hausdorffdistance", "st_maxdistance", "st_distance_sphere", "st_distance_spheroid", "st_dfullywithin", "st_dwithin", "st_equals", "st_hasarc", "st_intersects", "st_length", "st_length2d", "st_length3d", "st_length_spheroid", "st_length2d_spheroid", "st_length3d_spheroid", "st_longestline", "st_orderingequals", "st_overlaps", "st_perimeter", "st_perimeter2d", "st_perimeter3d", "st_pointonsurface", "st_relate", "st_shortestline", "st_touches", "st_within", | ||
| # 7.9. Geometry Processing Functions | ||
| "st_buffer", "st_buildarea", "st_collect", "st_convexhull", "st_curvetoline", "st_difference", "st_dump", "st_dumppoints", "st_dumprings", "st_intersection", "st_linetocurve", "st_memunion", "st_minimumboundingcircle", "st_polygonize", "st_shift_longitude", "st_simplify", "st_simplifypreservetopology", "st_symdifference", "st_union", | ||
| # 7.10. Linear Referencing | ||
| "st_line_interpolate_point", "st_line_locate_point", "st_line_substring", "st_locate_along_measure", "st_locate_between_measures", "st_locatebetweenelevations", "st_addmeasure", | ||
| # 7.11. Long Transactions Support | ||
| "addauth", "checkauth", "disablelongtransactions", "enablelongtransactions", "lockrow", "unlockrows", | ||
| # 7.12. Miscellaneous Functions | ||
| "st_accum", "box2d", "box3d", "st_estimated_extent", "st_expand", "st_extent", "st_extent3d", "find_srid", "st_mem_size", "st_point_inside_circle", "st_xmax", "st_xmin", "st_ymax", "st_ymin", "st_zmax", "st_zmin", | ||
| # 7.13. Exceptional Functions | ||
| "postgis_addbbox", "postgis_dropbbox", "postgis_hasbbox" | ||
| ] | ||
|
|
||
| # constants | ||
| constants = [ "null", "false", "true" ] | ||
| postgis_constants = [] | ||
|
|
||
| def getSqlDictionary(spatial=True): | ||
| k, c, f = list(keywords), list(constants), list(functions) | ||
|
|
||
| if spatial: | ||
| k += postgis_keywords | ||
| f += postgis_functions | ||
| c += postgis_constants | ||
|
|
||
| return { 'keyword' : k, 'constant' : c, 'function' : f } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| SET (DB_MANAGER_SPATIALITE_DIR ${DB_MANAGER_PLUGIN_DIR}/db_plugins/spatialite) | ||
|
|
||
| FILE(GLOB PY_FILES *.py) | ||
| FILE(GLOB ICON_FILES icons/*.png) | ||
|
|
||
| PYQT4_ADD_RESOURCES(PYRC_FILES resources.qrc) | ||
| ADD_CUSTOM_TARGET(db_manager_spatialite ALL DEPENDS ${PYRC_FILES}) | ||
|
|
||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_SPATIALITE_DIR}) | ||
| INSTALL(FILES ${PYRC_FILES} DESTINATION ${DB_MANAGER_SPATIALITE_DIR}) | ||
| INSTALL(FILES ${ICON_FILES} DESTINATION ${DB_MANAGER_SPATIALITE_DIR}/icons) |