| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| *.pyc | ||
| *~ | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| SET (DB_MANAGER_PLUGIN_DIR ${QGIS_DATA_DIR}/python/plugins/db_manager) | ||
|
|
||
| FILE(GLOB OTHER_FILES LICENCE README TODO) | ||
| FILE(GLOB PY_FILES *.py) | ||
|
|
||
| FILE(GLOB UI_FILES ui/*.ui) | ||
| PYQT4_WRAP_UI(PYUI_FILES ${UI_FILES}) | ||
| PYQT4_ADD_RESOURCES(PYRC_FILES resources.qrc) | ||
| ADD_CUSTOM_TARGET(db_manager ALL DEPENDS ${PYUI_FILES} ${PYRC_FILES}) | ||
|
|
||
| INSTALL(FILES ${OTHER_FILES} DESTINATION ${DB_MANAGER_PLUGIN_DIR}) | ||
| INSTALL(FILES ${PY_FILES} DESTINATION ${DB_MANAGER_PLUGIN_DIR}) | ||
| INSTALL(FILES ui/__init__.py ${PYUI_FILES} DESTINATION ${DB_MANAGER_PLUGIN_DIR}/ui) | ||
| INSTALL(FILES ${PYRC_FILES} DESTINATION ${DB_MANAGER_PLUGIN_DIR}) | ||
|
|
||
| ADD_SUBDIRECTORY(db_plugins) | ||
| ADD_SUBDIRECTORY(icons) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| CREDITS: 2011/10/13 | ||
|
|
||
| DB Manager plugin for QuantumGIS | ||
|
|
||
| Author and maintainer: | ||
| Giuseppe Sucameli <brush.tyler@gmail.com> | ||
|
|
||
|
|
||
| The following is a list of contributors who have participated in the project: | ||
|
|
||
| Version 0.1.18 includes: | ||
| - autocompletion based on "QTextEdit with autocompletion using pyqt" by rowinggolfer (see http://rowinggolfer.blogspot.com/2010/08/qtextedit-with-autocompletion-using.html) | ||
|
|
||
| Version 0.1.5 includes patches from: | ||
| - Sandro Santilli <strk@keybit.net> for plugin icon. | ||
|
|
||
| Version 0.1.2 includes: | ||
| - syntax highlighting based on "Python Syntax Highlighting Example" by Carson J. Q. Farmer (see http://www.carsonfarmer.com/?p=333) | ||
| - TopoViewer plugin based on qgis_pgis_topoview by Sandro Santilli <strk@keybit.net> (see at https://github.com/strk/qgis_pgis_topoview/) | ||
|
|
||
| Version 0.1.0 includes patches from: | ||
| - Mauricio de Paulo <mauricio.dev@gmail.com> for PostGIS Rasters support. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| DB Manager * Copyright (c) 2011 Giuseppe Sucameli | ||
|
|
||
| Licensed under the terms of GNU GPL v2 (or any layer) | ||
| http://www.gnu.org/copyleft/gpl.html | ||
|
|
||
|
|
||
| Code: | ||
| - some code is derived from PG_Manager by Martin Dobias (GPLv2 license) | ||
| - highlighter is based on "Python Syntax Highlighting Example" by Carson J. Q. Farmer (GPLv2 license) | ||
| - autocompletion based on "QTextEdit with autocompletion using pyqt" by rowinggolfer (GPLv2 license) | ||
|
|
||
| Icons: | ||
| - toolbar icons are derived from gis-0.1 iconset by Robert Szczepanek (Creative Commons Attribution-Share Alike 3.0 Unported license) | ||
| - refresh toolbar icon is from Tango project (public domain) | ||
| - table, view and namespace icons in database view are from pgAdmin3 (BSD license) | ||
| - other icons are from QGIS project (GPLv2 license) | ||
| - plugin icon by Sandro Santilli, using qgis icon and database icon by Dracos - http://commons.wikimedia.org/wiki/File:Applications-database.svg (Creative Commons Attribution-Share Alike 3.0 Unported license) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| DB Manager * Copyright (c) 2011 Giuseppe Sucameli | ||
|
|
||
| DB Manager is a database manager plugin for QuantumGIS. | ||
| It allows to you to show the DBs contents and run query on them. | ||
|
|
||
| In this moment DB Manager supports the following DBMS backends: | ||
| - PostgreSQL/PostGIS through the psycopg2 pymodule | ||
| - SQLite/SpatiaLite using the pyspatialite pymodule | ||
|
|
||
|
|
||
| For more info about the project, see at the wiki page: | ||
| http://www.qgis.org/wiki/DB_Manager_plugin_GSoC_2011 | ||
|
|
||
| or visit my GitHub repository: | ||
| https://github.com/brushtyler/db_manager | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| DB Manager TODO and DONE list. | ||
|
|
||
|
|
||
| DONE: | ||
| * run only the selected part of a query (v0.1.20) | ||
| * add versioning support to PostgreSQL dbs (v0.1.19) | ||
| * completer for sql keywords/functions (v0.1.18) | ||
| * highlights PG and SL functions, fix slow connection to PG db (v0.1.17) | ||
| * add contestual menu to db tree, use service param when available to connect to PG dbs (v0.1.16) | ||
| * close transactions before doing changes to tables (v0.1.15) | ||
| * improve error handling running a query in sql window (v0.1.14) | ||
| * fix error dialog (v0.1.13) | ||
| * improve error handling, add Re-connect button (v0.1.12) | ||
| * add "Run Vacuum" and "Move to schema" to menu (v0.1.11) | ||
| * fix encoding support and import layer on Win (v0.1.10) | ||
| * allow to copy contents from views (v0.1.9) | ||
| * GUI to import data by drag'n'drop (v0.1.8) | ||
| * edit table (v0.1.8) | ||
| * create table (v0.1.7) | ||
| * display schemas and tables comments (v0.1.6) | ||
| * SQL syntax highlighting (v0.1.5) | ||
| * load a query as layer into canvas (v0.1.4) | ||
| * import/export OGR layers and non-spatial data using Import Vector Layer feature (v.0.1.0) | ||
|
|
||
|
|
||
| TODO: | ||
| - PGManager | ||
| * GUI to import/export data (from shapefiles) | ||
| - RT_Sql_Layer | ||
| * query builder | ||
| * query manager | ||
| - QSpatialite | ||
| * GUI to import QGis layer | ||
| - SPIT | ||
| * mass import of shapefiles |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # -*- 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. * | ||
| * * | ||
| ***************************************************************************/ | ||
| """ | ||
|
|
||
| def name(): | ||
| return "DB Manager" | ||
|
|
||
| def description(): | ||
| return "Manage your databases within QGis" | ||
|
|
||
| def version(): | ||
| return "0.1.19" | ||
|
|
||
| def qgisMinimumVersion(): | ||
| return "1.5.0" | ||
|
|
||
| def icon(): | ||
| return "icons/dbmanager.png" | ||
|
|
||
| def authorName(): | ||
| return "Giuseppe Sucameli" | ||
|
|
||
| def classFactory(iface): | ||
| from .db_manager_plugin import DBManagerPlugin | ||
| return DBManagerPlugin(iface) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| # -*- 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 | ||
| The content of this file is based on | ||
| - QTextEdit with autocompletion using pyqt by rowinggolfer (GPLv2 license) | ||
| see http://rowinggolfer.blogspot.com/2010/08/qtextedit-with-autocompletion-using.html | ||
| ***************************************************************************/ | ||
| /*************************************************************************** | ||
| * * | ||
| * 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.QtGui import * | ||
| from PyQt4.QtCore import * | ||
|
|
||
| class SqlCompleter(QCompleter): | ||
| def __init__(self, editor, db=None): | ||
| # get the wordlist | ||
| dictionary = None | ||
| if db: | ||
| dictionary = db.connector.getSqlDictionary() | ||
| if not dictionary: | ||
| # use the generic sql dictionary | ||
| from .sql_dictionary import getSqlDictionary | ||
| dictionary = getSqlDictionary() | ||
|
|
||
| wordlist = QStringList() | ||
| for name, value in dictionary.iteritems(): | ||
| wordlist << value | ||
|
|
||
| # setup the completer | ||
| QCompleter.__init__(self, sorted(wordlist), editor) | ||
| self.setModelSorting(QCompleter.CaseInsensitivelySortedModel) | ||
| self.setWrapAround(False) | ||
|
|
||
| if isinstance(editor, CompletionTextEdit): | ||
| editor.setCompleter(self) | ||
|
|
||
|
|
||
| class CompletionTextEdit(QTextEdit): | ||
| def __init__(self, *args, **kwargs): | ||
| QTextEdit.__init__(self, *args, **kwargs) | ||
| self.completer = None | ||
|
|
||
| def setCompleter(self, completer): | ||
| if self.completer: | ||
| self.disconnect(self.completer, 0, self, 0) | ||
| if not completer: | ||
| return | ||
|
|
||
| completer.setWidget(self) | ||
| completer.setCompletionMode(QCompleter.PopupCompletion) | ||
| completer.setCaseSensitivity(Qt.CaseInsensitive) | ||
| self.completer = completer | ||
| self.connect(self.completer, SIGNAL("activated(const QString&)"), self.insertCompletion) | ||
|
|
||
| def insertCompletion(self, completion): | ||
| tc = self.textCursor() | ||
| extra = completion.length() - self.completer.completionPrefix().length() | ||
| tc.movePosition(QTextCursor.Left) | ||
| tc.movePosition(QTextCursor.EndOfWord) | ||
| tc.insertText(completion.right(extra)) | ||
| self.setTextCursor(tc) | ||
|
|
||
| def textUnderCursor(self): | ||
| tc = self.textCursor() | ||
| tc.select(QTextCursor.WordUnderCursor) | ||
| return tc.selectedText() | ||
|
|
||
| def focusInEvent(self, event): | ||
| if self.completer: | ||
| self.completer.setWidget(self) | ||
| QTextEdit.focusInEvent(self, event) | ||
|
|
||
| def keyPressEvent(self, event): | ||
| if self.completer and self.completer.popup().isVisible(): | ||
| if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): | ||
| event.ignore() | ||
| return | ||
|
|
||
| # has ctrl-E or ctrl-space been pressed?? | ||
| isShortcut = event.modifiers() == Qt.ControlModifier and event.key() in (Qt.Key_E, Qt.Key_Space) | ||
| if not self.completer or not isShortcut: | ||
| QTextEdit.keyPressEvent(self, event) | ||
|
|
||
| # ctrl or shift key on it's own?? | ||
| ctrlOrShift = event.modifiers() in (Qt.ControlModifier, Qt.ShiftModifier) | ||
| if ctrlOrShift and event.text().isEmpty(): | ||
| # ctrl or shift key on it's own | ||
| return | ||
|
|
||
| eow = QString("~!@#$%^&*()+{}|:\"<>?,./;'[]\\-=") # end of word | ||
|
|
||
| hasModifier = event.modifiers() != Qt.NoModifier and not ctrlOrShift | ||
|
|
||
| completionPrefix = self.textUnderCursor() | ||
|
|
||
| if not isShortcut and (hasModifier or event.text().isEmpty() or | ||
| completionPrefix.length() < 3 or eow.contains(event.text().right(1))): | ||
| self.completer.popup().hide() | ||
| return | ||
|
|
||
| if completionPrefix != self.completer.completionPrefix(): | ||
| self.completer.setCompletionPrefix(completionPrefix) | ||
| popup = self.completer.popup() | ||
| popup.setCurrentIndex(self.completer.completionModel().index(0,0)) | ||
|
|
||
| cr = self.cursorRect() | ||
| cr.setWidth(self.completer.popup().sizeHintForColumn(0) | ||
| + self.completer.popup().verticalScrollBar().sizeHint().width()) | ||
| self.completer.complete(cr) # popup it up! | ||
|
|
||
| #if __name__ == "__main__": | ||
| # app = QApplication([]) | ||
| # te = CompletionTextEdit() | ||
| # SqlCompleter( te ) | ||
| # te.show() | ||
| # app.exec_() |
| 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> |