524 changes: 522 additions & 2 deletions i18n/qgis_es.ts

Large diffs are not rendered by default.

1,711 changes: 1,173 additions & 538 deletions i18n/qgis_et_EE.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_fa.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_fi.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_fr.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_gl_ES.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_he.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_hr_HR.ts

Large diffs are not rendered by default.

1,711 changes: 1,173 additions & 538 deletions i18n/qgis_hu.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_id.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_is.ts

Large diffs are not rendered by default.

1,856 changes: 1,247 additions & 609 deletions i18n/qgis_it.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_ja.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_ka_GE.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_ko_KR.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_lo.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_lt.ts

Large diffs are not rendered by default.

606 changes: 598 additions & 8 deletions i18n/qgis_lv.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_mn.ts

Large diffs are not rendered by default.

528 changes: 520 additions & 8 deletions i18n/qgis_nl.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_no.ts

Large diffs are not rendered by default.

3,585 changes: 2,117 additions & 1,468 deletions i18n/qgis_pl_PL.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_pt_BR.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_pt_PT.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_ro.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_ru.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_sk.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_sl_SI.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_sq_AL.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_sr_CS-Latn.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_sv.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_ta.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_th.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_tr.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_uk.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_vi.ts

Large diffs are not rendered by default.

1,705 changes: 1,166 additions & 539 deletions i18n/qgis_xh.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_zh_CN.ts

Large diffs are not rendered by default.

1,707 changes: 1,167 additions & 540 deletions i18n/qgis_zh_TW.ts

Large diffs are not rendered by default.

19 changes: 5 additions & 14 deletions python/core/qgsrasterlayer.sip
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,10 @@ public:
bool loadDefaultStyleFlag = true );

/** \brief [ data provider interface ] Constructor in provider mode */
QgsRasterLayer( int dummy,
const QString & baseName = QString(),
const QString & path = QString(),
const QString & providerLib = QString(),
const QStringList & layers = QStringList(),
const QStringList & styles = QStringList(),
const QString & format = QString(),
const QString & crs = QString());

QgsRasterLayer( const QString & uri,
const QString & baseName,
const QString & providerKey,
bool loadDefaultStyleFlag = true );

/** \brief The destructor */
~QgsRasterLayer();
Expand Down Expand Up @@ -195,11 +190,7 @@ public:
QString redBandName();

/** [ data provider interface ] Set the data provider */
void setDataProvider( const QString & provider,
const QStringList & layers,
const QStringList & styles,
const QString & format,
const QString & crs );
void setDataProvider( const QString & provider );

/** \brief Mutator for drawing style */
void setDrawingStyle( const DrawingStyle & theDrawingStyle );
Expand Down
2 changes: 1 addition & 1 deletion python/gui/qgisinterface.sip
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class QgisInterface : QObject
//! Add a raster layer given a raster layer file name
virtual QgsRasterLayer* addRasterLayer(QString rasterLayerPath, QString baseName = QString())=0;
//! Add a WMS layer
virtual QgsRasterLayer* addRasterLayer(const QString& url, const QString& layerName, const QString& providerKey, const QStringList& layers, const QStringList& styles, const QString& format, const QString& crs) = 0;
virtual QgsRasterLayer* addRasterLayer(const QString& uri, const QString& baseName, const QString& providerKey) = 0;

//! Add a project
virtual bool addProject(QString theProject)=0;
Expand Down
1 change: 1 addition & 0 deletions python/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ ADD_SUBDIRECTORY(mapserver_export)
ADD_SUBDIRECTORY(fTools)
ADD_SUBDIRECTORY(GdalTools)
ADD_SUBDIRECTORY(osm)
ADD_SUBDIRECTORY(db_manager)
3 changes: 3 additions & 0 deletions python/plugins/db_manager/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.pyc
*~

17 changes: 17 additions & 0 deletions python/plugins/db_manager/CMakeLists.txt
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)
23 changes: 23 additions & 0 deletions python/plugins/db_manager/CREDITS
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.

17 changes: 17 additions & 0 deletions python/plugins/db_manager/LICENSE
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)
16 changes: 16 additions & 0 deletions python/plugins/db_manager/README
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

35 changes: 35 additions & 0 deletions python/plugins/db_manager/TODO
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
43 changes: 43 additions & 0 deletions python/plugins/db_manager/__init__.py
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)
131 changes: 131 additions & 0 deletions python/plugins/db_manager/completer.py
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_()
400 changes: 400 additions & 0 deletions python/plugins/db_manager/db_manager.py

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions python/plugins/db_manager/db_manager_plugin.py
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

602 changes: 602 additions & 0 deletions python/plugins/db_manager/db_model.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions python/plugins/db_manager/db_plugins/CMakeLists.txt
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)
70 changes: 70 additions & 0 deletions python/plugins/db_manager/db_plugins/__init__.py
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()

225 changes: 225 additions & 0 deletions python/plugins/db_manager/db_plugins/connector.py
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 []

304 changes: 304 additions & 0 deletions python/plugins/db_manager/db_plugins/data_model.py
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

159 changes: 159 additions & 0 deletions python/plugins/db_manager/db_plugins/html_elems.py
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"&nbsp;%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">&nbsp;&nbsp; ', 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)

450 changes: 450 additions & 0 deletions python/plugins/db_manager/db_plugins/info_model.py

Large diffs are not rendered by default.

1,062 changes: 1,062 additions & 0 deletions python/plugins/db_manager/db_plugins/plugin.py

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/CMakeLists.txt
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)
Empty file.
910 changes: 910 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/connector.py

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/data_model.py
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

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
191 changes: 191 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/info_model.py
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)
358 changes: 358 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/plugin.py
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)
36 changes: 36 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/plugins/__init__.py
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

Large diffs are not rendered by default.

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 (&lt;TABLE&gt;_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()

5 changes: 5 additions & 0 deletions python/plugins/db_manager/db_plugins/postgis/resources.qrc
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>
Loading