├── .gitignore
├── package.sh
├── Discovery
├── discovery_logo.png
├── i18n
│ ├── discovery_ja.qm
│ └── discovery_ja.ts
├── discovery_logo_64.png
├── discovery_ts.pro
├── __init__.py
├── utils.py
├── locator_filter.py
├── gpkg_utils.py
├── metadata.txt
├── mssql_utils.py
├── oracle_utils.py
├── dbutils.py
├── LICENSE
├── config_dialog.py
├── config_dialog.ui
├── discoveryplugin.py
└── discovery_ts.pro.qtds
├── install_plugin_dev_win.bat
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
3 | .idea
4 |
5 | *.zip
6 |
7 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | rm -f Discovery.zip && cd Discovery && git archive --prefix=Discovery/ -o ../Discovery.zip HEAD
2 |
--------------------------------------------------------------------------------
/Discovery/discovery_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lutraconsulting/qgis-discovery-plugin/HEAD/Discovery/discovery_logo.png
--------------------------------------------------------------------------------
/Discovery/i18n/discovery_ja.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lutraconsulting/qgis-discovery-plugin/HEAD/Discovery/i18n/discovery_ja.qm
--------------------------------------------------------------------------------
/Discovery/discovery_logo_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lutraconsulting/qgis-discovery-plugin/HEAD/Discovery/discovery_logo_64.png
--------------------------------------------------------------------------------
/Discovery/discovery_ts.pro:
--------------------------------------------------------------------------------
1 | SOURCES = config_dialog.py\
2 | discoveryplugin.py \
3 | gpkg_utils.py \
4 | locator_filter.py \
5 | mssql_utils.py \
6 | utils.py
7 |
8 | FORMS = config_dialog.ui
9 |
10 | TRANSLATIONS = i18n/discovery_ja.ts
11 |
--------------------------------------------------------------------------------
/install_plugin_dev_win.bat:
--------------------------------------------------------------------------------
1 |
2 | SET DIR=%cd%
3 | SET QGIS_PROFILE=default
4 |
5 | SET PLUGIN=Discovery
6 | SET SRC=%DIR%\%PLUGIN%
7 |
8 | SET DEST=%UserProfile%\AppData\Roaming\QGIS\QGIS3\profiles\%QGIS_PROFILE%\python\plugins
9 |
10 | SET DEST_PLUGIN=%DEST%\%PLUGIN%
11 |
12 | rd %DEST_PLUGIN% /s /q
13 |
14 | xcopy %SRC% %DEST_PLUGIN% /s/i/h/e/k/f/c
15 |
16 |
--------------------------------------------------------------------------------
/Discovery/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Discovery Plugin
4 | #
5 | # Copyright (C) 2015 Lutra Consulting
6 | # info@lutraconsulting.co.uk
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 2 of the License, or
11 | # (at your option) any later version.
12 |
13 |
14 | def classFactory(iface):
15 | from .discoveryplugin import DiscoveryPlugin
16 |
17 | return DiscoveryPlugin(iface)
18 |
--------------------------------------------------------------------------------
/Discovery/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Discovery Plugin
4 | #
5 | # Copyright (C) 2020 Lutra Consulting
6 | # info@lutraconsulting.co.uk
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 2 of the License, or
11 | # (at your option) any later version.
12 |
13 |
14 | def is_number(s):
15 | """Return True if s is a number"""
16 | try:
17 | float(s)
18 | return True
19 | except ValueError:
20 | return False
21 | except TypeError:
22 | return False
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Discovery (QGIS plugin)
3 | =======================
4 |
5 |
6 |
7 | The Discovery plugin adds search capability to QGIS. Its key features are:
8 |
9 | - Connects directly to PostgreSQL / PostGIS (no reliance on web services)
10 | - Auto-completion of results
11 | - Flexible expression-based support for scales
12 | - Can use multiple fields to display result context
13 | - Simple GUI-based configuration
14 |
15 | We’d like to thank Tim Martin of Ordnance Survey for his original PostGIS Search plugin which gave us inspiration and formed the foundation of Discovery.
16 |
17 | ### Using Discovery
18 |
19 | To learn how to use this plugin, see:
20 | http://www.lutraconsulting.co.uk/products/discovery/
21 |
--------------------------------------------------------------------------------
/Discovery/locator_filter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Discovery Plugin
4 | #
5 | # Copyright (C) 2017 Lutra Consulting
6 | # info@lutraconsulting.co.uk
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 2 of the License, or
11 | # (at your option) any later version.
12 |
13 |
14 | from qgis.core import QgsLocatorFilter, QgsLocatorResult, QgsMessageLog
15 |
16 | from . import config_dialog, dbutils
17 |
18 |
19 | class DiscoveryLocatorFilter(QgsLocatorFilter):
20 | def __init__(self, plugin):
21 | QgsLocatorFilter.__init__(self, None)
22 | self.plugin = plugin
23 |
24 | def clone(self):
25 | return DiscoveryLocatorFilter(self.plugin)
26 |
27 | def name(self):
28 | return "discovery"
29 |
30 | def displayName(self):
31 | return "Discovery - search in PostGIS tables"
32 |
33 | def prefix(self):
34 | return "dis"
35 |
36 | def fetchResults(self, text, context, feedback):
37 |
38 | if len(text) < 3:
39 | return
40 |
41 | query_text, query_dict = dbutils.get_search_sql(
42 | text,
43 | self.plugin.postgisgeomcolumn,
44 | self.plugin.postgissearchcolumn,
45 | self.plugin.echosearchcolumn,
46 | self.plugin.postgisdisplaycolumn,
47 | self.plugin.extra_expr_columns,
48 | self.plugin.postgisschema,
49 | self.plugin.postgistable,
50 | self.plugin.escapespecchars,
51 | self.plugin.limit_results,
52 | )
53 |
54 | conn = self.plugin.get_db()
55 | if not conn:
56 | QgsMessageLog.logMessage("The Locator Bar filter is currently only supported on PostGIS", "Discovery")
57 | return
58 | cursor = conn.cursor()
59 | cursor.execute(query_text, query_dict)
60 |
61 | for row in cursor.fetchall():
62 |
63 | if feedback.isCanceled():
64 | return
65 |
66 | geom, epsg, suggestion_text = row[0], row[1], row[2]
67 | extra_data = {}
68 | for idx, extra_col in enumerate(self.plugin.extra_expr_columns):
69 | extra_data[extra_col] = row[3 + idx]
70 |
71 | res = QgsLocatorResult(self, suggestion_text, (geom, epsg, suggestion_text, extra_data))
72 | self.resultFetched.emit(res)
73 |
74 | def triggerResult(self, result):
75 | self.plugin.select_result(result.userData)
76 |
77 | def hasConfigWidget(self):
78 | return True
79 |
80 | def openConfigWidget(self, parent):
81 | dlg = config_dialog.ConfigDialog(parent)
82 | if dlg.exec_():
83 | dlg.write_config()
84 | self.plugin.read_config()
85 |
--------------------------------------------------------------------------------
/Discovery/gpkg_utils.py:
--------------------------------------------------------------------------------
1 | from osgeo import gdal, ogr
2 | from qgis.core import QgsExpression, QgsFeatureRequest, QgsMessageLog, QgsVectorLayer
3 |
4 | from .utils import is_number
5 |
6 |
7 | def list_gpkg_layers(pckg_path):
8 | if not pckg_path:
9 | return []
10 |
11 | layer_names = []
12 | ds = gdal.OpenEx(pckg_path)
13 | lyr_count = ds.GetLayerCount()
14 | for i in range(lyr_count):
15 | lyr = ds.GetLayer(i)
16 | if lyr.GetGeomType() == ogr.wkbNone:
17 | continue
18 | lyr_name = lyr.GetName()
19 | layer_names.append(lyr_name)
20 | ds = None
21 | return layer_names
22 |
23 |
24 | def list_gpkg_fields(gpkg_path, name, bar_warning=None):
25 | try:
26 | layer = QgsVectorLayer(gpkg_path + "|layername=" + name, name, "ogr")
27 | fields = layer.fields()
28 | columns = []
29 | for f in fields:
30 | columns.append(f.name())
31 | return columns
32 | except RuntimeError as e:
33 | if bar_warning:
34 | bar_warning("Cannot read GeoPackage layer!")
35 | return []
36 |
37 |
38 | def search_gpkg(search_text, search_field, echo_search_column, display_fields, extra_expr_columns, layer, limit):
39 | wildcarded_search_string = ""
40 | for part in search_text.split():
41 | wildcarded_search_string += "%" + part
42 | wildcarded_search_string += "%"
43 | expr_str = "{0} ILIKE '{1}'".format(search_field, wildcarded_search_string)
44 | expr = QgsExpression(expr_str)
45 | req = QgsFeatureRequest(expr)
46 | limit = limit if is_number(limit) else None
47 | if limit:
48 | req.setLimit(int(limit))
49 | it = layer.getFeatures(req)
50 | result = []
51 |
52 | for f in it:
53 | feature_info = []
54 | geom = f.geometry().asWkt()
55 |
56 | crs_auth_id = layer.crs().authid()
57 | try:
58 | # only the plain integer code is wanted later on
59 | epsg = int(crs_auth_id.lstrip("EPSG:"))
60 | except ValueError:
61 | QgsMessageLog.logMessage(f"{crs_auth_id} is not an EPSG code.", "Discovery")
62 | return []
63 |
64 | feature_info.append(geom)
65 | feature_info.append(epsg)
66 | available_fields = [field.name() for field in f.fields()]
67 |
68 | display_info = []
69 | if echo_search_column:
70 | display_info.append(str(f[search_field]))
71 | for field_name in display_fields:
72 | if f[field_name]:
73 | display_info.append(str(f[field_name]))
74 | feature_info.append(", ".join(display_info))
75 |
76 | for field_name in extra_expr_columns:
77 | if field_name in available_fields:
78 | feature_info.append(f[field_name])
79 | else:
80 | feature_info.append("")
81 | result.append(feature_info)
82 | return result
83 |
--------------------------------------------------------------------------------
/Discovery/metadata.txt:
--------------------------------------------------------------------------------
1 | [general]
2 | name=Discovery
3 | qgisMinimumVersion=3.0
4 | qgisMaximumVersion=3.99
5 | description=Provides search / gazetteer functionality in QGIS using PostGIS, MSSQL and Geopackage databases
6 | version=2.6.0
7 | author=Lutra Consulting
8 | email=info@lutraconsulting.co.uk
9 | homepage=http://www.lutraconsulting.co.uk/products/discovery/
10 | tracker=https://github.com/lutraconsulting/qgis-discovery-plugin/issues
11 | repository=https://github.com/lutraconsulting/qgis-discovery-plugin
12 | icon=discovery_logo.png
13 | experimental=True
14 | about=The Discovery plugin adds search capability to QGIS. Its key features are:
15 | - Connects directly to PostgreSQL / PostGIS / Geopackage / MSSQL (no reliance on web services)
16 | - Auto-completion of results
17 | - Flexible expression-based support for scales
18 | - Can use multiple fields to display result context
19 | - Simple GUI-based configuration
20 |
21 | changelog=2.6.0
22 | - fixed issues: #95, #84
23 | - fixed handling features without geometry or with an unknown geometry type
24 | - added support for Oracle databases
25 | - added translations
26 |
2.5.10
27 | - allow searching for numbers in PostgreSQL dbs by casting searched column to text
28 |
2.5.9
29 | - fix for Qt deprecation warnings
30 | - fix for result geometry transformation
31 |
2.5.8
32 | - fix marker creation error when loading the plugin
33 | - handle PostgreSQL db connection timeouts
34 |
2.5.7
35 | - Made highlight marker colour configurable at the config-level
36 |
2.5.6
37 | - Limit for search results is now per config parameter, i.e. users can set different limits for their configs
38 |
2.5.5
39 | - Configurable limit of fetched results number for all providers
40 |
2.5.4
41 | - Escaping special characters (i.e. backslash) is now optional - by default there is NO escaping.
42 | - Tidied-up strings in config dialog
43 |
2.5.3
44 | - Limited number of results returned by SQL Server provider to 1000
45 |
2.5.2
46 | - Fixed issue with backslashes in search string safe for postgres
47 |
2.5.1 - Minor bug fix
48 | - Fixed issue with global settings
49 |
2.5.0 - New features
50 | - Add postgresql service support
51 | - Added optional bar info about selected search result
52 |
2.4.3 - Minor bugfixes
53 | - Fixed issue with plain text username and password
54 | - Fixed issue with missing dsn
55 | - Fixed issue with switching connection types
56 | - Fixed empty connections combo
57 |
2.4.2 - Minor bugfixes
58 | - Fixed QGIS authentication configuration what username/password is in environment variables
59 | - Fixed empty connections combo
60 |
2.4.1 - Minor bugfixes
61 | - Fixed MS SQL query when there are just one or zero display columns
62 |
2.4.0 - New features
63 | - Added support for using MSSQL and Geopackage databases
64 |
2.3.0 - New features
65 | - Added support for using QGIS authentication configurations
66 |
2.2.1 - Minor bugfixes
67 | - Use semi-transparent rubber band for polygons
68 |
2.2.0 - New features
69 | - Support for linestring and polygon geometries (#34)
70 | - Support for materialized views (#29)
71 |
1.0 - Initial release
96 |
97 | tags=PostGIS, search, gazetteer
98 |
--------------------------------------------------------------------------------
/Discovery/i18n/discovery_ja.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ConfigDialog
6 |
7 |
8 | Configuration
9 | 設定
10 |
11 |
12 |
13 | Echo search column in results
14 | 検索結果に検索カラムを表示する
15 |
16 |
17 |
18 | File
19 | ファイル
20 |
21 |
22 |
23 | Search column
24 | 検索カラム
25 |
26 |
27 |
28 | BBOX expression
29 | BBOXの表現
30 |
31 |
32 |
33 | Scale expression
34 | スケール感
35 |
36 |
37 |
38 | Schema
39 | スキーマ
40 |
41 |
42 |
43 | Escape special characters in search text
44 | 検索テキストに含まれる特殊文字のエスケープ
45 |
46 |
47 |
48 | Table
49 | テーブル
50 |
51 |
52 |
53 | Connection
54 | 接続方法
55 |
56 |
57 |
58 | Geometry column
59 | ジオメトリ 列
60 |
61 |
62 |
63 | Display columns
64 | 表示列
65 |
66 |
67 |
68 | Browse...
69 | 閲覧する...
70 |
71 |
72 |
73 | Name
74 | 名前
75 |
76 |
77 |
78 | Highlight colour
79 | ハイライト色
80 |
81 |
82 |
83 | Show bar info and hide it after
84 | バー情報を表示し、その後非表示にする
85 |
86 |
87 |
88 |
89 | seconds
90 | 秒
91 |
92 |
93 |
94 | Auto-hide marker after
95 | マーカーを自動で隠す
96 |
97 |
98 |
99 | Copy selected item info to clipboard
100 | 選択した項目の情報をクリップボードにコピーする
101 |
102 |
103 |
104 | Limit fetched results number to
105 | 結果番号
106 |
107 |
108 |
109 | +
110 |
111 |
112 |
113 |
114 | -
115 |
116 |
117 |
118 |
119 | Data source type
120 | データソースの種類
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/Discovery/mssql_utils.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from PyQt5.QtSql import QSqlDatabase, QSqlQuery
4 | from qgis.core import QgsMessageLog, QgsSettings
5 |
6 | from . import dbutils
7 | from .utils import is_number
8 |
9 |
10 | def get_mssql_connections():
11 | """Read PostgreSQL connection names from QgsSettings stored by QGIS"""
12 | settings = QgsSettings()
13 | settings.beginGroup("/MSSQL/connections/")
14 | return settings.childGroups()
15 |
16 |
17 | def get_mssql_conn_info(connection):
18 | return connection
19 |
20 |
21 | def get_connection(conn_name, service, host, database, username, password):
22 | # inspired by creation of connection string from QGIS MS SQL provider
23 | db = QSqlDatabase.addDatabase("QODBC", "discovery_" + conn_name)
24 | db.setHostName(host)
25 | if service:
26 | connection_string = service
27 | else:
28 | if sys.platform.startswith("win"):
29 | connection_string = "driver={SQL Server}"
30 | else:
31 | connection_string = "driver={FreeTDS};port=1433"
32 | if host:
33 | connection_string += ";server=" + host
34 |
35 | if database:
36 | connection_string += ";database=" + database
37 | if not password:
38 | connection_string += ";trusted_connection=yes"
39 | else:
40 | connection_string += ";uid=" + username + ";pwd=" + password
41 | connection_string += ";TDS_Version=8.0;ClientCharset=UTF-8"
42 |
43 | if username:
44 | db.setUserName(username)
45 |
46 | if password:
47 | db.setPassword(password)
48 | db.setDatabaseName(connection_string)
49 |
50 | if not db.open():
51 | raise Exception(db.lastError().text())
52 | return db
53 |
54 |
55 | def get_mssql_conn(connection):
56 | settings = QgsSettings()
57 | settings.beginGroup("/MSSQL/connections/" + connection)
58 | service = settings.value("/service", "")
59 | host = settings.value("/host", "")
60 | database = settings.value("/database", "")
61 | username = settings.value("/username", "")
62 | password = settings.value("/password", "")
63 | return get_connection(connection, service, host, database, username, password)
64 |
65 |
66 | def list_schemas(db):
67 | """Get list of schema names"""
68 | query = QSqlQuery(db)
69 | query_text = """SELECT schema_name
70 | FROM information_schema.schemata
71 | WHERE schema_owner = 'dbo';""" # TODO: better way to filter out system schemas
72 | query.exec(query_text)
73 | names = []
74 | while query.next():
75 | names.append(query.value(0))
76 | return sorted(names)
77 |
78 |
79 | def list_tables(db):
80 | query_text = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
81 | query = QSqlQuery(db)
82 | query.exec(query_text)
83 | names = []
84 | while query.next():
85 | names.append(query.value(0))
86 | return names
87 |
88 |
89 | def list_columns(db, schema, table):
90 | query_text = """SELECT COLUMN_NAME
91 | FROM INFORMATION_SCHEMA.COLUMNS
92 | WHERE TABLE_NAME = '%s' AND TABLE_SCHEMA ='%s';""" % (
93 | dbutils._quote_str(table),
94 | dbutils._quote_str(schema),
95 | )
96 | query = QSqlQuery(db)
97 | query.exec(query_text)
98 | names = []
99 | while query.next():
100 | names.append(query.value(0))
101 | return names
102 |
103 |
104 | def _quote_brackets(identifier):
105 | """quote identifier as []"""
106 | return "[%s]" % identifier.replace('"', '""')
107 |
108 |
109 | def get_search_sql(
110 | search_text,
111 | geom_column,
112 | search_column,
113 | echo_search_column,
114 | display_columns,
115 | extra_expr_columns,
116 | schema,
117 | table,
118 | limit,
119 | ):
120 | wildcarded_search_string = ""
121 | for part in search_text.split():
122 | wildcarded_search_string += "%" + part
123 | wildcarded_search_string += "%"
124 | limit = "{}".format(int(limit)) if is_number(limit) else "1000"
125 | query_text = """ SELECT TOP %s
126 | [%s].STAsText() AS geom,
127 | [%s].STSrid AS epsg,
128 | """ % (
129 | limit,
130 | geom_column,
131 | geom_column,
132 | )
133 |
134 | info_columns = []
135 | if echo_search_column:
136 | info_columns.append(_quote_brackets(search_column))
137 | if len(display_columns) > 0:
138 | for display_column in display_columns.split(","):
139 | info_columns.append(_quote_brackets(display_column))
140 |
141 | if len(info_columns) == 0:
142 | query_text += "'' AS suggestion_string"
143 | elif len(info_columns) == 1:
144 | query_text += "CAST (%s AS nvarchar) AS suggestion_string" % info_columns[0]
145 | else:
146 | joined_info_columns = ", ', ' COLLATE Latin1_General_CI_AS, ".join(info_columns)
147 | query_text += "CONCAT( %s ) AS suggestion_string" % joined_info_columns
148 |
149 | for extra_column in extra_expr_columns:
150 | query_text += ", [%s]" % extra_column
151 | query_text += """
152 | FROM
153 | [%s].[%s]
154 | WHERE [%s] LIKE
155 | """ % (
156 | schema,
157 | table,
158 | search_column,
159 | )
160 | query_text += (
161 | """ '%s'
162 | """
163 | % wildcarded_search_string
164 | )
165 | query_text += (
166 | """ORDER BY
167 | [%s]
168 | """
169 | % search_column
170 | )
171 |
172 | return query_text
173 |
174 |
175 | def execute(db, query_text):
176 | query = QSqlQuery(db)
177 | if not query.exec(query_text):
178 | QgsMessageLog.logMessage(query.lastError().text() + "\n\nQuery:\n" + query_text, "Discovery")
179 | return []
180 |
181 | record = query.record()
182 | result_set = []
183 | while query.next():
184 | row = []
185 | for i in range(record.count()):
186 | row.append(query.value(i))
187 | result_set.append(row)
188 | return result_set
189 |
--------------------------------------------------------------------------------
/Discovery/oracle_utils.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtSql import QSqlDatabase, QSqlQuery
2 | from qgis.core import QgsMessageLog, QgsSettings
3 |
4 | from . import dbutils
5 | from .utils import is_number
6 |
7 |
8 | def get_oracle_connections():
9 | """Read Oracle connection names from QgsSettings stored by QGIS"""
10 | settings = QgsSettings()
11 | settings.beginGroup("/Oracle/connections/")
12 | return settings.childGroups()
13 |
14 |
15 | def get_oracle_conn_info(connection):
16 | return connection
17 |
18 |
19 | def get_connection(conn_name, host, database, port, username, password):
20 | """Connect to the database using conn_info dict:
21 | { 'host': ..., 'port': ..., 'database': ..., 'username': ..., 'password': ... }
22 | """
23 | db = QSqlDatabase.addDatabase("QOCISPATIAL", "discovery_" + conn_name)
24 | connection_string = ""
25 |
26 | if host:
27 | connection_string = host
28 |
29 | if port not in ("1521",):
30 | connection_string = connection_string + ":" + port
31 |
32 | if database:
33 | connection_string = connection_string + "/" + database
34 |
35 | db.setDatabaseName(connection_string)
36 | db.setUserName(username)
37 | db.setPassword(password)
38 |
39 | if not db.open():
40 | raise Exception(db.lastError().text())
41 | return db
42 |
43 |
44 | def get_oracle_conn(connection):
45 | settings = QgsSettings()
46 | settings.beginGroup("/Oracle/connections/" + connection)
47 | host = settings.value("/host", "")
48 | database = settings.value("/database", "")
49 | port = settings.value("/port", "")
50 | username = settings.value("/username", "")
51 | password = settings.value("/password", "")
52 | return get_connection(connection, host, database, port, username, password)
53 |
54 |
55 | def list_schemas(db):
56 | """Get list of schema names"""
57 | query = QSqlQuery(db)
58 | query_text = """SELECT u.username
59 | FROM all_users u
60 | WHERE EXISTS (SELECT null FROM all_tables t WHERE t.owner = u.username)
61 | ORDER BY u.username""" # TODO: better way to filter out system schemas
62 | query.exec(query_text)
63 | names = []
64 | while query.next():
65 | names.append(query.value(0))
66 | return names
67 |
68 |
69 | def list_tables(db, schema):
70 | query_text = """SELECT object_name
71 | FROM all_objects
72 | WHERE object_type IN ('TABLE', 'VIEW')
73 | AND NOT REGEXP_LIKE (object_name, '^DR\\$|^MDRT_|^MDXT_')
74 | AND owner = '%s'
75 | ORDER BY object_name""" % dbutils._quote_str(
76 | schema
77 | )
78 | query = QSqlQuery(db)
79 | query.exec(query_text)
80 | names = []
81 | while query.next():
82 | names.append(query.value(0))
83 | return names
84 |
85 |
86 | def list_columns(db, schema, table):
87 | query_text = """SELECT column_name
88 | FROM all_tab_columns
89 | WHERE table_name = '%s' AND owner ='%s'
90 | ORDER BY column_id""" % (
91 | dbutils._quote_str(table),
92 | dbutils._quote_str(schema),
93 | )
94 | query = QSqlQuery(db)
95 | query.exec(query_text)
96 | names = []
97 | while query.next():
98 | names.append(query.value(0))
99 | return names
100 |
101 |
102 | def _quote(identifier):
103 | """quote identifier"""
104 | return '"%s"' % identifier.replace('"', '""')
105 |
106 |
107 | def get_search_sql(
108 | search_text,
109 | geom_column,
110 | search_column,
111 | echo_search_column,
112 | display_columns,
113 | extra_expr_columns,
114 | schema,
115 | table,
116 | limit,
117 | ):
118 | """Returns a tuple: (SQL query text, dictionary with values to replace variables with)."""
119 |
120 | """
121 | Spaces in queries
122 | A query with spaces is executed as follows:
123 | 'my query'
124 | LIKE '%my%query%'
125 |
126 | A note on spaces in postcodes
127 | Postcodes must be stored in the DB without spaces:
128 | 'DL10 4DQ' becomes 'DL104DQ'
129 | This allows users to query with or without spaces
130 | As wildcards are inserted at spaces, it doesn't matter whether the query is:
131 | 'dl10 4dq'; or
132 | 'dl104dq'
133 | """
134 | wildcarded_search_string = ""
135 | for part in search_text.split():
136 | wildcarded_search_string += "%" + part
137 | wildcarded_search_string += "%"
138 | query_dict = {"search_text": wildcarded_search_string}
139 | query_text = """ SELECT
140 | SDO_UTIL.TO_WKTGEOMETRY("%s") AS geom,
141 | S."%s"."SDO_SRID" AS epsg,
142 | """ % (
143 | geom_column,
144 | geom_column,
145 | )
146 |
147 | if echo_search_column:
148 | query_column_selection_text = (
149 | """"%s"
150 | """
151 | % search_column
152 | )
153 | suggestion_string_seperator = ", "
154 | else:
155 | query_column_selection_text = """''"""
156 | suggestion_string_seperator = ""
157 | if len(display_columns) > 0:
158 | for display_column in display_columns.split(","):
159 | query_column_selection_text += """ || CASE WHEN "%s" IS NOT NULL THEN
160 | '%s' || "%s"
161 | ELSE
162 | ''
163 | END
164 | """ % (
165 | display_column,
166 | suggestion_string_seperator,
167 | display_column,
168 | )
169 | suggestion_string_seperator = ", "
170 | query_column_selection_text += """ AS suggestion_string """
171 | if query_column_selection_text.startswith("'', "):
172 | query_column_selection_text = query_column_selection_text[4:]
173 | query_text += query_column_selection_text
174 | for extra_column in extra_expr_columns:
175 | query_text += ', "%s"' % extra_column
176 | query_text += """
177 | FROM
178 | "%s"."%s" S
179 | WHERE
180 | LOWER("%s") LIKE
181 | """ % (
182 | schema,
183 | table,
184 | search_column,
185 | )
186 | query_text += """ LOWER('%s')
187 | """ % (
188 | wildcarded_search_string
189 | )
190 |
191 | limit = "{}".format(int(limit)) if is_number(limit) else "1000"
192 | query_text += """AND ROWNUM <= %s""" % (limit)
193 | query_text += (
194 | """ORDER BY
195 | "%s"
196 | """
197 | % search_column
198 | )
199 | return query_text
200 |
201 |
202 | def execute(db, query_text):
203 | query = QSqlQuery(db)
204 | if not query.exec(query_text):
205 | QgsMessageLog.logMessage(query.lastError().text() + "\n\nQuery:\n" + query_text, "Discovery")
206 | return []
207 |
208 | record = query.record()
209 | result_set = []
210 | while query.next():
211 | row = []
212 | for i in range(record.count()):
213 | row.append(query.value(i))
214 | result_set.append(row)
215 | return result_set
216 |
--------------------------------------------------------------------------------
/Discovery/dbutils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Discovery Plugin
4 | #
5 | # Copyright (C) 2015 Lutra Consulting
6 | # info@lutraconsulting.co.uk
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 2 of the License, or
11 | # (at your option) any later version.
12 |
13 | import re
14 |
15 | import psycopg2
16 | from qgis.core import QgsApplication, QgsAuthMethodConfig, QgsSettings
17 |
18 | from .utils import is_number
19 |
20 |
21 | def get_connection(conn_info):
22 | """Connect to the database using conn_info dict:
23 | { 'host': ..., 'port': ..., 'database': ..., 'username': ..., 'password': ... }
24 | """
25 | conn = psycopg2.connect(**conn_info)
26 | conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
27 | return conn
28 |
29 |
30 | def get_postgres_connections():
31 | """Read PostgreSQL connection names from QgsSettings stored by QGIS"""
32 | settings = QgsSettings()
33 | settings.beginGroup("/PostgreSQL/connections/")
34 | return settings.childGroups()
35 |
36 |
37 | """
38 | def current_postgres_connection():
39 | settings = QgsSettings()
40 | settings.beginGroup("/Discovery")
41 | return settings.value("connection", "", type=str)
42 | """
43 |
44 |
45 | def get_postgres_conn_info(selected):
46 | """Read PostgreSQL connection details from QgsSettings stored by QGIS"""
47 | settings = QgsSettings()
48 | settings.beginGroup("/PostgreSQL/connections/" + selected)
49 | if not settings.contains("database"): # non-existent entry?
50 | return {}
51 |
52 | conn_info = dict()
53 |
54 | # Check if a service is provided
55 | service = settings.value("service", "", type=str)
56 | hasService = len(service) > 0
57 | if hasService:
58 | conn_info["service"] = service
59 |
60 | # password and username
61 | username = ""
62 | password = ""
63 | authconf = settings.value("authcfg", "")
64 | if authconf:
65 | # password encrypted in AuthManager
66 | auth_manager = QgsApplication.authManager()
67 | conf = QgsAuthMethodConfig()
68 | auth_manager.loadAuthenticationConfig(authconf, conf, True)
69 | if conf.id():
70 | username = conf.config("username", "")
71 | password = conf.config("password", "")
72 | else:
73 | # basic (plain-text) settings
74 | username = settings.value("username", "", type=str)
75 | password = settings.value("password", "", type=str)
76 |
77 | # password and username could be stored in environment variables
78 | # if not present in AuthManager or plain-text settings, do not
79 | # add it to conn_info at all
80 | if len(username) > 0:
81 | conn_info["user"] = username
82 | if len(password) > 0:
83 | conn_info["password"] = password
84 |
85 | host = settings.value("host", "", type=str)
86 | database = settings.value("database", "", type=str)
87 | port = settings.value("port", "", type=str)
88 |
89 | # Prevent setting host, port or database to empty string or default value
90 | # It may by set in a provided service and would overload it
91 | if len(host) > 0:
92 | conn_info["host"] = host
93 | if len(database) > 0:
94 | conn_info["database"] = database
95 | if len(port) > 0:
96 | conn_info["port"] = int(port)
97 |
98 | return conn_info
99 |
100 |
101 | def _quote(identifier):
102 | """quote identifier"""
103 | return '"%s"' % identifier.replace('"', '""')
104 |
105 |
106 | def _quote_str(txt):
107 | """make the string safe - replace ' with ''"""
108 | return txt.replace("'", "''")
109 |
110 |
111 | def list_schemas(cursor):
112 | """Get list of schema names"""
113 | sql = "SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_' AND nspname != 'information_schema'"
114 | cursor.execute(sql)
115 |
116 | names = map(lambda row: row[0], cursor.fetchall())
117 | return sorted(names)
118 |
119 |
120 | def list_tables(cursor, schema):
121 | sql = """SELECT pg_class.relname
122 | FROM pg_class
123 | JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
124 | WHERE pg_class.relkind IN ('v', 'r', 'm') AND nspname = '%s'
125 | ORDER BY nspname, relname""" % _quote_str(
126 | schema
127 | )
128 | cursor.execute(sql)
129 | names = map(lambda row: row[0], cursor.fetchall())
130 | return sorted(names)
131 |
132 |
133 | def list_columns(cursor, schema, table):
134 | sql = """SELECT a.attname AS column_name
135 | FROM pg_class c
136 | JOIN pg_attribute a ON a.attrelid = c.oid
137 | JOIN pg_namespace nsp ON c.relnamespace = nsp.oid
138 | WHERE c.relname = '%s' AND nspname='%s' AND a.attnum > 0
139 | ORDER BY a.attnum""" % (
140 | _quote_str(table),
141 | _quote_str(schema),
142 | )
143 | cursor.execute(sql)
144 | names = map(lambda row: row[0], cursor.fetchall())
145 | return sorted(names)
146 |
147 |
148 | def get_search_sql(
149 | search_text,
150 | geom_column,
151 | search_column,
152 | echo_search_column,
153 | display_columns,
154 | extra_expr_columns,
155 | schema,
156 | table,
157 | escape_spec_chars,
158 | limit,
159 | ):
160 | """Returns a tuple: (SQL query text, dictionary with values to replace variables with)."""
161 |
162 | """
163 | Spaces in queries
164 | A query with spaces is executed as follows:
165 | 'my query'
166 | ILIKE '%my%query%'
167 |
168 | A note on spaces in postcodes
169 | Postcodes must be stored in the DB without spaces:
170 | 'DL10 4DQ' becomes 'DL104DQ'
171 | This allows users to query with or without spaces
172 | As wildcards are inserted at spaces, it doesn't matter whether the query is:
173 | 'dl10 4dq'; or
174 | 'dl104dq'
175 | """
176 |
177 | # escape search text to allow \ backslash characters in search string
178 | # i.e. 1\TP => 1\\TP
179 | if escape_spec_chars:
180 | search_text = re.escape(search_text)
181 |
182 | wildcarded_search_string = ""
183 | for part in search_text.split():
184 | wildcarded_search_string += "%" + part
185 | wildcarded_search_string += "%"
186 | query_dict = {"search_text": wildcarded_search_string}
187 |
188 | query_text = """ SELECT
189 | ST_AsText("%s") AS geom,
190 | ST_SRID("%s") AS epsg,
191 | """ % (
192 | geom_column,
193 | geom_column,
194 | )
195 | if echo_search_column:
196 | query_column_selection_text = (
197 | """"%s"
198 | """
199 | % search_column
200 | )
201 | suggestion_string_seperator = ", "
202 | else:
203 | query_column_selection_text = """''"""
204 | suggestion_string_seperator = ""
205 | if len(display_columns) > 0:
206 | for display_column in display_columns.split(","):
207 | query_column_selection_text += """ || CASE WHEN "%s" IS NOT NULL THEN
208 | '%s' || "%s"
209 | ELSE
210 | ''
211 | END
212 | """ % (
213 | display_column,
214 | suggestion_string_seperator,
215 | display_column,
216 | )
217 | suggestion_string_seperator = ", "
218 | query_column_selection_text += """ AS suggestion_string """
219 | if query_column_selection_text.startswith("'', "):
220 | query_column_selection_text = query_column_selection_text[4:]
221 | query_text += query_column_selection_text
222 | for extra_column in extra_expr_columns:
223 | query_text += ', "%s"' % extra_column
224 | query_text += """
225 | FROM
226 | "%s"."%s"
227 | WHERE
228 | "%s"::text ILIKE
229 | """ % (
230 | schema,
231 | table,
232 | search_column,
233 | )
234 | query_text += """ %(search_text)s
235 | """
236 |
237 | limit = "{}".format(int(limit)) if is_number(limit) else "1000"
238 | query_text += """ORDER BY
239 | "%s"
240 | LIMIT %s
241 | """ % (
242 | search_column,
243 | limit,
244 | )
245 |
246 | return query_text, query_dict
247 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/Discovery/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/Discovery/config_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Discovery Plugin
4 | #
5 | # Copyright (C) 2015 Lutra Consulting
6 | # info@lutraconsulting.co.uk
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 2 of the License, or
11 | # (at your option) any later version.
12 |
13 | import os
14 |
15 | from PyQt5 import uic
16 | from PyQt5.QtCore import QSettings, Qt, QUrl
17 | from PyQt5.QtGui import QColor, QDesktopServices
18 | from PyQt5.QtWidgets import QApplication, QDialog, QDialogButtonBox, QFileDialog, QMessageBox
19 | from qgis.core import QgsSettings
20 |
21 | from . import dbutils, discoveryplugin, gpkg_utils, mssql_utils, oracle_utils
22 |
23 | plugin_dir = os.path.dirname(__file__)
24 |
25 | uiConfigDialog, qtBaseClass = uic.loadUiType(os.path.join(plugin_dir, "config_dialog.ui"))
26 |
27 |
28 | class ConfigDialog(qtBaseClass, uiConfigDialog):
29 |
30 | def __init__(self, parent=None):
31 | qtBaseClass.__init__(self, parent)
32 | self.setupUi(self)
33 |
34 | self.conn = None
35 | self.key = "" # currently selected config key
36 |
37 | # signals
38 | self.buttonBox.button(QDialogButtonBox.Help).clicked.connect(self.show_help)
39 | self.addButton.clicked.connect(self.add_config)
40 | self.deleteButton.clicked.connect(self.delete_config)
41 | self.configOptions.currentIndexChanged.connect(self.config_selection_changed)
42 | self.cboName.textChanged.connect(self.validate_nameField)
43 | self.cboDataSource.currentIndexChanged.connect(self.data_type_changed)
44 | self.fileButton.clicked.connect(self.browse_file_db)
45 | self.cboFile.currentIndexChanged.connect(self.populate_tables)
46 | self.cboSchema.currentIndexChanged.connect(self.populate_tables)
47 | self.cboTable.currentIndexChanged.connect(self.populate_columns)
48 |
49 | settings = QgsSettings()
50 | settings.beginGroup("/Discovery")
51 |
52 | # init config list
53 | if not settings.value("config_list"):
54 | settings.setValue("config_list", [])
55 | config_list = settings.value("config_list")
56 |
57 | # prev version compatibility settings
58 | if self.prev_version_config_available():
59 | config_list.append("")
60 | settings.setValue("config_list", config_list)
61 |
62 | # if empty, add config
63 | if not config_list:
64 | config_list.append("New config")
65 | settings.setValue("config_list", config_list)
66 |
67 | self.init_cbo_data_source()
68 | self.cboConnection.currentIndexChanged.connect(self.connect_db)
69 | self.cboConnection.addItem("")
70 | self.populate_connections()
71 |
72 | for key in config_list:
73 | self.configOptions.addItem(key)
74 |
75 | if self.configOptions.count():
76 | self.configOptions.setCurrentIndex(0)
77 |
78 | self.key = self.configOptions.currentText() if self.configOptions.currentIndex() >= 0 else ""
79 |
80 | if not self.configOptions.count():
81 | self.enable_form(False)
82 |
83 | self.chkMarkerTime.stateChanged.connect(self.time_checkbox_changed)
84 | self.chkBarInfoTime.stateChanged.connect(self.bar_info_checkbox_changed)
85 |
86 | def init_cbo_data_source(self):
87 | self.cboDataSource.addItem("PostgreSQL", "postgres")
88 | self.cboDataSource.addItem("MS SQL Server", "mssql")
89 | self.cboDataSource.addItem("Oracle", "oracle")
90 | self.cboDataSource.addItem("GeoPackage", "gpkg")
91 | self.cboDataSource.setCurrentIndex(0)
92 |
93 | def prev_version_config_available(self):
94 | settings = QSettings()
95 | settings.beginGroup("/Discovery")
96 |
97 | conn = settings.value("connection")
98 | if conn:
99 | return True
100 | return False
101 |
102 | def validate_nameField(self):
103 | settings = QSettings()
104 | settings.beginGroup("/Discovery")
105 | config_list = settings.value("config_list")
106 | key = self.cboName.text()
107 |
108 | if self.validate_key(key, config_list):
109 | self.cboName.setStyleSheet("")
110 | self.lblMessage.setText("")
111 | else:
112 | self.lblMessage.setText("Connection name is too short or already exists!")
113 | self.cboName.setStyleSheet("QLineEdit {background-color: pink;}")
114 |
115 | # connected to buttonBox.accepted()
116 | def validate_and_accept(self):
117 | settings = QSettings()
118 | settings.beginGroup("/Discovery")
119 | config_list = settings.value("config_list")
120 | key = self.cboName.text()
121 |
122 | if self.validate_key(key, config_list):
123 | self.accept()
124 | else:
125 | self.cboName.setStyleSheet("QLineEdit {background-color: pink;}")
126 |
127 | def reset_form_fields(self):
128 | self.cboName.setText("")
129 | self.cboDataSource.setCurrentIndex(0)
130 | self.enable_fields_for_data_type()
131 | self.init_conn_schema_cbos([], "")
132 | self.cboTable.setCurrentIndex(0)
133 | self.populate_columns()
134 |
135 | for cbo in [
136 | self.cboSearchColumn,
137 | self.cboGeomColumn,
138 | self.cboDisplayColumn1,
139 | self.cboDisplayColumn2,
140 | self.cboDisplayColumn3,
141 | self.cboDisplayColumn4,
142 | self.cboDisplayColumn5,
143 | ]:
144 | cbo.setCurrentIndex(0)
145 |
146 | def set_form_fields(self, key):
147 | QApplication.setOverrideCursor(Qt.WaitCursor)
148 | settings = QSettings()
149 | settings.beginGroup("/Discovery")
150 |
151 | if key:
152 | self.cboName.setText(key)
153 | else:
154 | self.cboName.setText("")
155 |
156 | data_type = settings.value(key + "data_type", "postgres")
157 | data_type_idx = self.cboDataSource.findData(data_type)
158 | self.cboDataSource.blockSignals(True)
159 | self.cboDataSource.setCurrentIndex(data_type_idx)
160 | self.cboDataSource.blockSignals(False)
161 |
162 | self.populate_connections()
163 |
164 | # tables
165 | self.init_combo_from_settings(self.cboTable, key + "table")
166 | self.populate_columns()
167 |
168 | # columns
169 | self.init_combo_from_settings(self.cboSearchColumn, key + "search_column")
170 | if data_type in ("postgres", "mssql", "oracle"):
171 | self.label_3.setText("Table")
172 | self.cboGeomColumn.setEnabled(True)
173 | self.init_combo_from_settings(self.cboGeomColumn, key + "geom_column")
174 | elif data_type == "gpkg":
175 | self.label_3.setText("Layer")
176 | self.cboGeomColumn.clear()
177 | self.cboGeomColumn.addItem("")
178 | self.cboGeomColumn.setEnabled(False)
179 |
180 | self.enable_fields_for_data_type()
181 |
182 | escape_spec_chars = settings.value(key + "escape_spec_chars", False, type=bool)
183 | self.cbEscapeSpecChars.setCheckState(Qt.Checked if escape_spec_chars else Qt.Unchecked)
184 | echo_search_col = settings.value(key + "echo_search_column", True, type=bool)
185 | self.cbEchoSearchColumn.setCheckState(Qt.Checked if echo_search_col else Qt.Unchecked)
186 |
187 | columns = settings.value(key + "display_columns", "", type=str)
188 | if len(columns) != 0:
189 | lst = columns.split(",")
190 | self.set_combo_current_text(self.cboDisplayColumn1, lst[0])
191 | if len(lst) > 1:
192 | self.set_combo_current_text(self.cboDisplayColumn2, lst[1])
193 | if len(lst) > 2:
194 | self.set_combo_current_text(self.cboDisplayColumn3, lst[2])
195 | if len(lst) > 3:
196 | self.set_combo_current_text(self.cboDisplayColumn4, lst[3])
197 | if len(lst) > 4:
198 | self.set_combo_current_text(self.cboDisplayColumn5, lst[4])
199 |
200 | self.editScaleExpr.setText(settings.value(key + "scale_expr", "", type=str))
201 | self.editBboxExpr.setText(settings.value(key + "bbox_expr", "", type=str))
202 | h_color = QColor()
203 | h_color.setNamedColor(settings.value(key + "highlight_color", "#FF0000", type=str))
204 | self.color_picker.setColor(h_color)
205 | self.chkMarkerTime.setChecked(settings.value("marker_time_enabled", True, type=bool))
206 | self.spinMarkerTime.setValue(settings.value("marker_time", 5000, type=int) // 1000)
207 | self.chkBarInfoTime.setChecked(settings.value("bar_info_time_enabled", True, type=bool))
208 | self.spinBarInfoTime.setValue(settings.value("bar_info_time", 30, type=int))
209 | self.spinLimitResults.setValue(settings.value(key + "limit_results", 1000, type=int))
210 | self.time_checkbox_changed()
211 | self.bar_info_checkbox_changed()
212 | self.chkInfoToClipboard.setChecked(settings.value("info_to_clipboard", True, type=bool))
213 |
214 | QApplication.restoreOverrideCursor()
215 |
216 | def init_conn_schema_cbos(self, current_connections, key):
217 | all_cons = [self.cboConnection.itemText(i) for i in range(self.cboConnection.count())]
218 | self.cboConnection.clear()
219 | for conn in current_connections:
220 | if conn not in all_cons:
221 | self.cboConnection.addItem(conn)
222 | self.init_combo_from_settings(self.cboConnection, key + "connection")
223 | self.connect_db()
224 | # schemas
225 | self.init_combo_from_settings(self.cboSchema, key + "schema")
226 | self.populate_tables()
227 |
228 | def init_combo_from_settings(self, cbo, settings_key):
229 | settings = QSettings()
230 | settings.beginGroup("/Discovery")
231 | name = settings.value(settings_key, "", type=str)
232 | self.set_combo_current_text(cbo, name)
233 |
234 | def set_combo_current_text(self, cbo, name):
235 | idx = cbo.findText(name)
236 | cbo.setCurrentIndex(idx) if idx != -1 else cbo.setEditText(name)
237 |
238 | def connect_db(self):
239 | name = self.cboConnection.currentText()
240 | if name == "":
241 | return
242 | try:
243 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
244 | if data_type == "postgres":
245 | self.conn = dbutils.get_connection(dbutils.get_postgres_conn_info(name))
246 | elif data_type == "mssql":
247 | self.conn = mssql_utils.get_mssql_conn(mssql_utils.get_mssql_conn_info(name))
248 | elif data_type == "oracle":
249 | self.conn = oracle_utils.get_oracle_conn(oracle_utils.get_oracle_conn_info(name))
250 | self.lblMessage.setText("")
251 | except Exception as e:
252 | self.conn = None
253 | self.lblMessage.setText("" + str(e) + "")
254 | self.populate_schemas()
255 |
256 | def populate_connections(self):
257 | key = self.cboName.text()
258 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
259 | if data_type == "postgres":
260 | current_connections = dbutils.get_postgres_connections()
261 | self.init_conn_schema_cbos(current_connections, key)
262 | elif data_type == "mssql":
263 | current_connections = mssql_utils.get_mssql_connections()
264 | self.init_conn_schema_cbos(current_connections, key)
265 | elif data_type == "oracle":
266 | current_connections = oracle_utils.get_oracle_connections()
267 | self.init_conn_schema_cbos(current_connections, key)
268 | elif data_type == "gpkg":
269 | self.init_combo_from_settings(self.cboFile, key + "file")
270 | self.populate_tables()
271 |
272 | def populate_schemas(self):
273 | self.cboSchema.clear()
274 | self.cboSchema.addItem("")
275 | if self.conn is None:
276 | return
277 |
278 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
279 | if data_type == "postgres":
280 | schemas = dbutils.list_schemas(self.conn.cursor())
281 | elif data_type == "mssql":
282 | schemas = mssql_utils.list_schemas(self.conn)
283 | elif data_type == "oracle":
284 | schemas = oracle_utils.list_schemas(self.conn)
285 | else:
286 | schemas = []
287 | for schema in schemas:
288 | self.cboSchema.addItem(schema)
289 |
290 | def populate_tables(self):
291 | self.cboTable.clear()
292 | self.cboTable.addItem("")
293 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
294 | if data_type == "postgres":
295 | if self.conn is None:
296 | return
297 | tables = dbutils.list_tables(self.conn.cursor(), self.cboSchema.currentText())
298 | elif data_type == "mssql":
299 | if self.conn is None:
300 | return
301 | tables = mssql_utils.list_tables(self.conn) # TODO: filter by schema
302 | elif data_type == "oracle":
303 | if self.conn is None:
304 | return
305 | tables = oracle_utils.list_tables(self.conn, self.cboSchema.currentText())
306 | elif data_type == "gpkg":
307 | tables = gpkg_utils.list_gpkg_layers(self.cboFile.currentText())
308 | else:
309 | return # current index == -1
310 |
311 | for table in tables:
312 | self.cboTable.addItem(table)
313 |
314 | def populate_columns(self):
315 | cbos = [
316 | self.cboSearchColumn,
317 | self.cboGeomColumn,
318 | self.cboDisplayColumn1,
319 | self.cboDisplayColumn2,
320 | self.cboDisplayColumn3,
321 | self.cboDisplayColumn4,
322 | self.cboDisplayColumn5,
323 | ]
324 | for cbo in cbos:
325 | cbo.clear()
326 | cbo.addItem("")
327 |
328 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
329 | if data_type == "postgres":
330 | if self.conn is None:
331 | return
332 | columns = dbutils.list_columns(
333 | self.conn.cursor(), self.cboSchema.currentText(), self.cboTable.currentText()
334 | )
335 | elif data_type == "mssql":
336 | if self.conn is None:
337 | return
338 | columns = mssql_utils.list_columns(self.conn, self.cboSchema.currentText(), self.cboTable.currentText())
339 | elif data_type == "oracle":
340 | if self.conn is None:
341 | return
342 | columns = oracle_utils.list_columns(self.conn, self.cboSchema.currentText(), self.cboTable.currentText())
343 | elif data_type == "gpkg":
344 | columns = gpkg_utils.list_gpkg_fields(self.cboFile.currentText(), self.cboTable.currentText())
345 | else:
346 | return # current index == -1
347 |
348 | for cbo in cbos:
349 | for column in columns:
350 | cbo.addItem(column)
351 |
352 | def enable_fields_for_data_type(self):
353 | data_type = self.cboDataSource.itemData(self.cboDataSource.currentIndex())
354 | is_db = data_type in ("mssql", "oracle", "postgres")
355 |
356 | for w in [self.cboConnection, self.cboSchema, self.label, self.label_2]:
357 | w.setEnabled(is_db)
358 | w.setVisible(is_db)
359 | for w in [self.file_grid_layout, self.cboFile, self.label_10, self.fileButton]:
360 | w.setEnabled(not is_db)
361 | w.setVisible(not is_db)
362 |
363 | def validate_key(self, key, config_list):
364 | if not key:
365 | return False
366 | if self.key != key and key in config_list:
367 | return False
368 |
369 | return True
370 |
371 | def write_config(self):
372 |
373 | settings = QSettings()
374 | settings.beginGroup("/Discovery")
375 |
376 | config_list = settings.value("config_list")
377 | if not config_list:
378 | config_list = []
379 |
380 | key = self.cboName.text()
381 | if self.key != key:
382 |
383 | if not self.validate_key(key, config_list):
384 | return
385 |
386 | if self.key in config_list:
387 | config_list.remove(self.key)
388 | discoveryplugin.delete_config_from_settings(self.key, settings)
389 | self.key = key
390 |
391 | if key not in config_list:
392 | config_list.append(key)
393 | settings.setValue("config_list", config_list)
394 |
395 | settings.setValue(key + "data_type", self.cboDataSource.itemData(self.cboDataSource.currentIndex()))
396 | settings.setValue(key + "file", self.cboFile.currentText())
397 | settings.setValue(key + "connection", self.cboConnection.currentText())
398 | settings.setValue(key + "schema", self.cboSchema.currentText())
399 | settings.setValue(key + "table", self.cboTable.currentText())
400 | settings.setValue(key + "search_column", self.cboSearchColumn.currentText())
401 | settings.setValue(key + "escape_spec_chars", self.cbEscapeSpecChars.isChecked())
402 | settings.setValue(key + "echo_search_column", self.cbEchoSearchColumn.isChecked())
403 | settings.setValue(key + "display_columns", self.display_columns())
404 | settings.setValue(key + "geom_column", self.cboGeomColumn.currentText())
405 | settings.setValue(key + "scale_expr", self.editScaleExpr.text())
406 | settings.setValue(key + "bbox_expr", self.editBboxExpr.text())
407 | settings.setValue(key + "highlight_color", self.color_picker.color().name())
408 |
409 | settings.setValue("marker_time_enabled", self.chkMarkerTime.isChecked())
410 | settings.setValue("marker_time", self.spinMarkerTime.value() * 1000)
411 | settings.setValue("bar_info_time_enabled", self.chkBarInfoTime.isChecked())
412 | settings.setValue("bar_info_time", self.spinBarInfoTime.value())
413 | settings.setValue(key + "limit_results", self.spinLimitResults.value())
414 | settings.setValue("info_to_clipboard", self.chkInfoToClipboard.isChecked())
415 |
416 | self.configOptions.clear()
417 | for k in config_list:
418 | self.configOptions.addItem(k)
419 |
420 | index = self.configOptions.findText(key)
421 | if index != -1:
422 | self.configOptions.setCurrentIndex(index)
423 |
424 | def time_checkbox_changed(self):
425 | self.spinMarkerTime.setEnabled(self.chkMarkerTime.isChecked())
426 |
427 | def bar_info_checkbox_changed(self):
428 | self.spinBarInfoTime.setEnabled(self.chkBarInfoTime.isChecked())
429 |
430 | def display_columns(self):
431 | """Make a string out of display columns, e.g. "column1,column2" or just "column1" """
432 | lst = []
433 | for cbo in [
434 | self.cboDisplayColumn1,
435 | self.cboDisplayColumn2,
436 | self.cboDisplayColumn3,
437 | self.cboDisplayColumn4,
438 | self.cboDisplayColumn5,
439 | ]:
440 | txt = cbo.currentText()
441 | if len(txt) > 0:
442 | lst.append(txt)
443 | return ",".join(lst)
444 |
445 | def enable_form(self, enable=True):
446 | self.datasource_lout.setEnabled(enable)
447 |
448 | def add_config(self):
449 | txt = ""
450 | self.configOptions.addItem(txt)
451 | self.configOptions.setCurrentIndex(self.configOptions.count() - 1)
452 |
453 | settings = QSettings()
454 | settings.beginGroup("/Discovery")
455 | config_list = settings.value("config_list")
456 | if not (config_list):
457 | config_list = []
458 | self.enable_form()
459 | config_list.append(txt)
460 | settings.setValue("config_list", config_list)
461 |
462 | # reset fields
463 | self.reset_form_fields()
464 | self.cboName.setText(txt)
465 | self.cboDataSource.setCurrentIndex(0)
466 | self.populate_connections()
467 | self.key = txt
468 |
469 | def delete_config(self):
470 | if self.configOptions.currentIndex() < 0:
471 | return
472 |
473 | msgBox = QMessageBox()
474 | msgBox.setWindowTitle("Delete configuration")
475 | msgBox.setText("Do you want to delete selected configuration?")
476 | msgBox.setStandardButtons(QMessageBox.Yes)
477 | msgBox.addButton(QMessageBox.No)
478 | msgBox.setDefaultButton(QMessageBox.No)
479 | if msgBox.exec_() == QMessageBox.No:
480 | return
481 |
482 | self.delete_config_without_confirm()
483 |
484 | def delete_config_without_confirm(self):
485 | item_text = self.configOptions.currentText()
486 | self.configOptions.removeItem(self.configOptions.currentIndex())
487 | settings = QSettings()
488 | settings.beginGroup("/Discovery")
489 | config_list = settings.value("config_list")
490 | config_list.remove(item_text)
491 | settings.setValue("config_list", config_list)
492 | if self.configOptions.count():
493 | self.configOptions.setCurrentIndex(0)
494 | else:
495 | self.reset_form_fields()
496 | self.enable_form(False)
497 | self.key = ""
498 |
499 | def config_selection_changed(self):
500 | if not self.configOptions.count():
501 | return
502 | if self.configOptions.currentIndex() < 0:
503 | return
504 |
505 | self.key = self.configOptions.currentText()
506 | self.set_form_fields(self.key)
507 |
508 | def data_type_changed(self):
509 | self.conn = None
510 | self.populate_connections()
511 | self.enable_fields_for_data_type()
512 |
513 | def browse_file_db(self):
514 | dialog = QFileDialog(self)
515 | dialog.setWindowTitle("Open GeoPackage database")
516 | dialog.setNameFilters(["*.gpkg"])
517 | dialog.setFileMode(QFileDialog.ExistingFile)
518 | if dialog.exec_() == QDialog.Accepted:
519 | filename = dialog.selectedFiles()[0]
520 | if self.cboFile.findText(filename) < 0:
521 | self.cboFile.addItem(filename)
522 | self.cboFile.setCurrentIndex(self.cboFile.findText(filename))
523 |
524 | def show_help(self):
525 | QDesktopServices.openUrl(QUrl("http://www.lutraconsulting.co.uk/products/discovery/"))
526 |
--------------------------------------------------------------------------------
/Discovery/config_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | ConfigDialog
4 |
5 |
6 | true
7 |
8 |
9 |
10 | 0
11 | 0
12 | 453
13 | 693
14 |
15 |
16 |
17 |
18 | 0
19 | 0
20 |
21 |
22 |
23 |
24 | 0
25 | 0
26 |
27 |
28 |
29 | Configuration
30 |
31 |
32 |
33 |
34 |
35 |
36 | 0
37 | 0
38 |
39 |
40 |
41 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
42 |
43 |
44 | true
45 |
46 |
47 |
48 |
49 |
50 |
51 | 0
52 |
53 |
54 |
55 |
56 | border-bottom: 1px solid black
57 |
58 |
59 |
60 | 1
61 |
62 |
63 | 0
64 |
65 |
66 | 0
67 |
68 |
69 | 0
70 |
71 |
72 |
73 |
74 | 0
75 |
76 |
77 | 0
78 |
79 |
80 |
81 |
82 |
83 | 0
84 | 0
85 |
86 |
87 |
88 | true
89 |
90 |
91 | QComboBox::NoInsert
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 0
100 | 0
101 |
102 |
103 |
104 | true
105 |
106 |
107 | QComboBox::NoInsert
108 |
109 |
110 |
111 |
112 |
113 |
114 | border-bottom:0px
115 |
116 |
117 | Echo search column in results
118 |
119 |
120 | true
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 0
129 | 0
130 |
131 |
132 |
133 | true
134 |
135 |
136 | QComboBox::NoInsert
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | 0
145 | 0
146 |
147 |
148 |
149 | true
150 |
151 |
152 | QComboBox::NoInsert
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | 0
161 | 0
162 |
163 |
164 |
165 | true
166 |
167 |
168 | QComboBox::NoInsert
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | 0
180 | 0
181 |
182 |
183 |
184 | true
185 |
186 |
187 | QComboBox::NoInsert
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | 0
196 | 0
197 |
198 |
199 |
200 |
201 | 16777215
202 | 21
203 |
204 |
205 |
206 | File
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | 0
215 | 0
216 |
217 |
218 |
219 | true
220 |
221 |
222 | QComboBox::NoInsert
223 |
224 |
225 |
226 |
227 |
228 |
229 | Search column
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 | 0
238 | 0
239 |
240 |
241 |
242 | true
243 |
244 |
245 | QComboBox::NoInsert
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | BBOX expression
256 |
257 |
258 |
259 |
260 |
261 |
262 | Scale expression
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 | 0
271 | 0
272 |
273 |
274 |
275 | true
276 |
277 |
278 | QComboBox::NoInsert
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 | Schema
289 |
290 |
291 |
292 |
293 |
294 |
295 | border-bottom:0px
296 |
297 |
298 | Escape special characters in search text
299 |
300 |
301 |
302 |
303 |
304 |
305 | Table
306 |
307 |
308 |
309 |
310 |
311 |
312 | Connection
313 |
314 |
315 |
316 |
317 |
318 |
319 | Geometry column
320 |
321 |
322 |
323 |
324 |
325 |
326 | Display columns
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 | 0
335 | 0
336 |
337 |
338 |
339 | true
340 |
341 |
342 | QComboBox::NoInsert
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 | 0
351 | 0
352 |
353 |
354 |
355 |
356 | 0
357 | 0
358 |
359 |
360 |
361 |
362 | 16777215
363 | 16777215
364 |
365 |
366 |
367 | border-bottom:0px;
368 |
369 |
370 |
371 |
372 | QLayout::SetNoConstraint
373 |
374 |
375 | 0
376 |
377 |
378 | 0
379 |
380 |
381 | 0
382 |
383 |
384 | 0
385 |
386 |
387 |
388 |
389 | true
390 |
391 |
392 |
393 | 4
394 | 0
395 |
396 |
397 |
398 |
399 |
400 |
401 | true
402 |
403 |
404 | QComboBox::NoInsert
405 |
406 |
407 | true
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 | 0
416 | 0
417 |
418 |
419 |
420 |
421 | 0
422 | 0
423 |
424 |
425 |
426 |
427 | 16777215
428 | 16777215
429 |
430 |
431 |
432 |
433 |
434 |
435 | Browse...
436 |
437 |
438 |
439 | 16
440 | 16
441 |
442 |
443 |
444 | true
445 |
446 |
447 | false
448 |
449 |
450 | false
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 | Name
461 |
462 |
463 |
464 |
465 |
466 |
467 | Highlight colour
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 | 0
485 |
486 |
487 |
488 |
489 | Show bar info and hide it after
490 |
491 |
492 |
493 |
494 |
495 |
496 | seconds
497 |
498 |
499 | 1
500 |
501 |
502 | 9999
503 |
504 |
505 | 30
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 | Auto-hide marker after
517 |
518 |
519 |
520 |
521 |
522 |
523 | seconds
524 |
525 |
526 | 1
527 |
528 |
529 | 5
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 | Copy selected item info to clipboard
539 |
540 |
541 |
542 |
543 |
544 |
545 | 0
546 |
547 |
548 |
549 |
550 | Limit fetched results number to
551 |
552 |
553 |
554 |
555 |
556 |
557 | 1
558 |
559 |
560 | 9999999
561 |
562 |
563 | 1000
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 | Qt::Horizontal
577 |
578 |
579 | QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 | +
594 |
595 |
596 |
597 |
598 |
599 |
600 | -
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 | 0
612 |
613 |
614 |
615 |
616 | Data source type
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 | 0
625 | 0
626 |
627 |
628 |
629 | false
630 |
631 |
632 | QComboBox::NoInsert
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 | Qt::Vertical
642 |
643 |
644 |
645 | 20
646 | 20
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 | QgsColorButton
656 | QToolButton
657 | qgscolorbutton.h
658 |
659 |
660 |
661 | buttonBox
662 |
663 |
664 |
665 |
666 | buttonBox
667 | accepted()
668 | ConfigDialog
669 | validate_and_accept()
670 |
671 |
672 | 257
673 | 732
674 |
675 |
676 | 157
677 | 274
678 |
679 |
680 |
681 |
682 | buttonBox
683 | rejected()
684 | ConfigDialog
685 | reject()
686 |
687 |
688 | 325
689 | 732
690 |
691 |
692 | 286
693 | 274
694 |
695 |
696 |
697 |
698 |
699 | validate_and_accept()
700 | clear_and_reject()
701 |
702 |
703 |
--------------------------------------------------------------------------------
/Discovery/discoveryplugin.py:
--------------------------------------------------------------------------------
1 | # Discovery Plugin
2 | #
3 | # Copyright (C) 2020 Lutra Consulting
4 | # info@lutraconsulting.co.uk
5 | #
6 | # Thanks to Tim Martin of Ordnance Survey for his original PostGIS Search
7 | # plugin which inspired and formed the foundation of Discovery.
8 | #
9 | # This program is free software; you can redistribute it and/or modify
10 | # it under the terms of the GNU General Public License as published by
11 | # the Free Software Foundation; either version 2 of the License, or
12 | # (at your option) any later version.
13 |
14 | import os.path
15 | import time
16 |
17 | import psycopg2
18 | from PyQt5.QtCore import QCoreApplication, QModelIndex, QSettings, Qt, QTimer, QTranslator, QVariant
19 | from PyQt5.QtGui import QColor, QIcon
20 | from PyQt5.QtWidgets import QAction, QApplication, QComboBox, QCompleter, QMessageBox
21 | from qgis.core import (
22 | Qgis,
23 | QgsCoordinateReferenceSystem,
24 | QgsCoordinateTransform,
25 | QgsExpression,
26 | QgsExpressionContext,
27 | QgsFeature,
28 | QgsField,
29 | QgsFields,
30 | QgsGeometry,
31 | QgsRectangle,
32 | QgsSettings,
33 | QgsVectorLayer,
34 | QgsWkbTypes,
35 | )
36 | from qgis.gui import QgsFilterLineEdit, QgsRubberBand, QgsVertexMarker
37 | from qgis.utils import iface
38 |
39 | from Discovery import gpkg_utils, mssql_utils, oracle_utils
40 |
41 | from . import config_dialog, dbutils, locator_filter
42 |
43 |
44 | def eval_expression(expr_text, extra_data, default=None):
45 | """Helper method to evaluate an expression. E.g.
46 | eval_expression("1+a", {"a": 2}) will return 3
47 | """
48 | if expr_text is None or len(expr_text) == 0:
49 | return default
50 |
51 | flds = QgsFields()
52 | for extra_col, extra_value in extra_data.items():
53 | if isinstance(extra_value, int):
54 | t = QVariant.Int
55 | elif isinstance(extra_value, float):
56 | t = QVariant.Double
57 | else:
58 | t = QVariant.String
59 | flds.append(QgsField(extra_col, t))
60 | f = QgsFeature(flds)
61 | for extra_col, extra_value in extra_data.items():
62 | f[extra_col] = extra_value
63 | expr = QgsExpression(expr_text)
64 | ctx = QgsExpressionContext()
65 | ctx.setFeature(f)
66 | res = expr.evaluate(ctx)
67 | return default if expr.hasEvalError() else res
68 |
69 |
70 | def bbox_str_to_rectangle(bbox_str):
71 | """Helper method to convert "xmin,ymin,xmax,ymax" to QgsRectangle - or return None on error"""
72 | if bbox_str is None or len(bbox_str) == 0:
73 | return None
74 |
75 | coords = bbox_str.split(",")
76 | if len(coords) != 4:
77 | return None
78 |
79 | try:
80 | xmin = float(coords[0])
81 | ymin = float(coords[1])
82 | xmax = float(coords[2])
83 | ymax = float(coords[3])
84 | return QgsRectangle(xmin, ymin, xmax, ymax)
85 | except ValueError:
86 | return None
87 |
88 |
89 | def delete_config_from_settings(key, settings):
90 | settings.remove(key + "data_type")
91 | settings.remove(key + "file")
92 | settings.remove(key + "connection")
93 | settings.remove(key + "schema")
94 | settings.remove(key + "table")
95 | settings.remove(key + "search_column")
96 | settings.remove(key + "escape_spec_chars")
97 | settings.remove(key + "echo_search_column")
98 | settings.remove(key + "display_columns")
99 | settings.remove(key + "geom_column")
100 | settings.remove(key + "scale_expr")
101 | settings.remove(key + "bbox_expr")
102 |
103 |
104 | class DiscoveryPlugin:
105 |
106 | def __init__(self, _iface):
107 | # Save reference to the QGIS interface
108 | self.iface = _iface
109 | # initialize plugin directory
110 | self.plugin_dir = os.path.dirname(__file__)
111 |
112 | # Localize
113 | locale = QSettings().value("locale/userLocale")[0:2]
114 | localePath = os.path.join(self.plugin_dir, "i18n", "discovery_{}.qm".format(locale))
115 | if os.path.exists(localePath):
116 | self.translator = QTranslator()
117 | self.translator.load(localePath)
118 | QCoreApplication.installTranslator(self.translator)
119 |
120 | # Variables to facilitate delayed queries and database connection management
121 | self.db_timer = QTimer()
122 | self.line_edit_timer = QTimer()
123 | self.line_edit_timer.setSingleShot(True)
124 | self.line_edit_timer.timeout.connect(self.reset_line_edit_after_move)
125 | self.next_query_time = None
126 | self.last_query_time = time.time()
127 | self.db_conn = None
128 | self.search_delay = 0.5 # s
129 | self.query_sql = ""
130 | self.query_text = ""
131 | self.query_dict = {}
132 | self.db_idle_time = 60.0 # s
133 | self.display_time = 5000 # ms
134 | self.bar_info_time = 30 # s
135 |
136 | self.search_results = []
137 | self.limit_results = 1000
138 | self.tool_bar = None
139 | self.search_line_edit = None
140 | self.completer = None
141 | self.conn_info = {}
142 |
143 | self.marker = QgsVertexMarker(iface.mapCanvas())
144 | self.marker.setIconSize(15)
145 | self.marker.setPenWidth(2)
146 | self.marker.setColor(QColor(226, 27, 28)) # 51,160,44))
147 | self.marker.setZValue(11)
148 | self.marker.setVisible(False)
149 | self.marker2 = QgsVertexMarker(iface.mapCanvas())
150 | self.marker2.setIconSize(16)
151 | self.marker2.setPenWidth(4)
152 | self.marker2.setColor(QColor(255, 255, 255, 200))
153 | self.marker2.setZValue(10)
154 | self.marker2.setVisible(False)
155 | self.is_displayed = False
156 |
157 | self.rubber_band = QgsRubberBand(iface.mapCanvas(), QgsWkbTypes.PolygonGeometry)
158 | self.rubber_band.setVisible(False)
159 | self.rubber_band.setWidth(3)
160 | self.rubber_band.setStrokeColor(QColor(226, 27, 28))
161 | self.rubber_band.setFillColor(QColor(226, 27, 28, 63))
162 |
163 | def initGui(self):
164 |
165 | # Create a new toolbar
166 | self.tool_bar = self.iface.addToolBar("Discovery")
167 | self.tool_bar.setObjectName("Discovery_Plugin")
168 |
169 | # Create action that will start plugin configuration
170 | self.action_config = QAction(
171 | QIcon(os.path.join(self.plugin_dir, "discovery_logo.png")), "Configure Discovery", self.tool_bar
172 | )
173 | self.action_config.triggered.connect(self.show_config_dialog)
174 | self.tool_bar.addAction(self.action_config)
175 |
176 | # Add combobox for configs
177 | self.config_combo = QComboBox()
178 | settings = QgsSettings()
179 | settings.beginGroup("/Discovery")
180 | config_list = settings.value("config_list")
181 |
182 | if config_list:
183 | for conf in config_list:
184 | self.config_combo.addItem(conf)
185 | elif settings.childGroups():
186 | # support for prev version
187 | key = "Config1"
188 | config_list = []
189 | config_list.append(key)
190 | settings.setValue("config_list", config_list)
191 | self.config_combo.addItem(key)
192 |
193 | settings.setValue(key + "data_type", settings.value("data_type"))
194 | settings.setValue(key + "file", settings.value("file"))
195 | settings.setValue(key + "connection", settings.value("connection"))
196 | settings.setValue(key + "schema", settings.value("schema"))
197 | settings.setValue(key + "table", settings.value("table"))
198 | settings.setValue(key + "search_column", settings.value("search_column"))
199 | settings.setValue(key + "escape_spec_chars", settings.value("escape_spec_chars"))
200 | settings.setValue(key + "echo_search_column", settings.value("echo_search_column"))
201 | settings.setValue(key + "display_columns", settings.value("display_columns"))
202 | settings.setValue(key + "geom_column", settings.value("geom_column"))
203 | settings.setValue(key + "scale_expr", settings.value("scale_expr"))
204 | settings.setValue(key + "bbox_expr", settings.value("bbox_expr"))
205 |
206 | delete_config_from_settings("", settings)
207 | self.tool_bar.addWidget(self.config_combo)
208 |
209 | # Add search edit box
210 | self.search_line_edit = QgsFilterLineEdit()
211 | self.search_line_edit.setPlaceholderText("Search for...")
212 | self.search_line_edit.setMaximumWidth(768)
213 | self.tool_bar.addWidget(self.search_line_edit)
214 |
215 | self.config_combo.currentIndexChanged.connect(self.change_configuration)
216 |
217 | # Set up the completer
218 | self.completer = QCompleter([]) # Initialise with en empty list
219 | self.completer.setCaseSensitivity(Qt.CaseInsensitive)
220 | self.completer.setMaxVisibleItems(1000)
221 | self.completer.setModelSorting(QCompleter.UnsortedModel) # Sorting done in PostGIS
222 | self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) # Show all fetched possibilities
223 | self.completer.activated[QModelIndex].connect(self.on_result_selected)
224 | self.completer.highlighted[QModelIndex].connect(self.on_result_highlighted)
225 | self.search_line_edit.setCompleter(self.completer)
226 |
227 | # Connect any signals
228 | self.search_line_edit.textEdited.connect(self.on_search_text_changed)
229 |
230 | # Search results
231 | self.search_results = []
232 |
233 | # Set up a timer to periodically perform db queries as required
234 | self.db_timer.timeout.connect(self.do_db_operations)
235 | self.db_timer.start(100)
236 |
237 | # Read config
238 | self.read_config(config_list[0] if config_list else "")
239 |
240 | self.locator_filter = locator_filter.DiscoveryLocatorFilter(self)
241 | self.iface.registerLocatorFilter(self.locator_filter)
242 |
243 | # Debug
244 | # import pydevd; pydevd.settrace('localhost', port=5678)
245 |
246 | def unload(self):
247 | # Stop timer
248 | self.db_timer.stop()
249 | # Disconnect any signals
250 | self.db_timer.timeout.disconnect(self.do_db_operations)
251 | self.completer.highlighted[QModelIndex].disconnect(self.on_result_highlighted)
252 | self.completer.activated[QModelIndex].disconnect(self.on_result_selected)
253 | self.search_line_edit.textEdited.disconnect(self.on_search_text_changed)
254 | # Remove the new toolbar
255 | self.tool_bar.clear() # Clear all actions
256 | self.iface.mainWindow().removeToolBar(self.tool_bar)
257 |
258 | self.iface.deregisterLocatorFilter(self.locator_filter)
259 | self.locator_filter = None
260 |
261 | def clear_suggestions(self):
262 | model = self.completer.model()
263 | model.setStringList([])
264 |
265 | def on_search_text_changed(self, new_search_text):
266 | """
267 | This function is called whenever the user modified the search text
268 |
269 | 1. Open a database connection
270 | 2. Make the query
271 | 3. Update the QStringListModel with these results
272 | 4. Store the other details in self.search_results
273 | """
274 |
275 | self.query_text = new_search_text
276 |
277 | if len(new_search_text) < 3:
278 | # Clear any previous suggestions in case the user is 'backspacing'
279 | self.clear_suggestions()
280 | return
281 |
282 | if self.data_type == "postgres":
283 | query_text, query_dict = dbutils.get_search_sql(
284 | new_search_text,
285 | self.postgisgeomcolumn,
286 | self.postgissearchcolumn,
287 | self.echosearchcolumn,
288 | self.postgisdisplaycolumn,
289 | self.extra_expr_columns,
290 | self.postgisschema,
291 | self.postgistable,
292 | self.escapespecchars,
293 | self.limit_results,
294 | )
295 | self.schedule_search(query_text, query_dict)
296 |
297 | elif self.data_type == "gpkg":
298 | query_text = (
299 | new_search_text,
300 | self.postgissearchcolumn,
301 | self.echosearchcolumn,
302 | self.postgisdisplaycolumn.split(","),
303 | self.extra_expr_columns,
304 | self.layer,
305 | self.limit_results,
306 | )
307 | self.schedule_search(query_text, None)
308 |
309 | elif self.data_type == "mssql":
310 | query_text = mssql_utils.get_search_sql(
311 | new_search_text,
312 | self.postgisgeomcolumn,
313 | self.postgissearchcolumn,
314 | self.echosearchcolumn,
315 | self.postgisdisplaycolumn,
316 | self.extra_expr_columns,
317 | self.postgisschema,
318 | self.postgistable,
319 | self.limit_results,
320 | )
321 | self.schedule_search(query_text, None)
322 |
323 | elif self.data_type == "oracle":
324 | query_text = oracle_utils.get_search_sql(
325 | new_search_text,
326 | self.postgisgeomcolumn,
327 | self.postgissearchcolumn,
328 | self.echosearchcolumn,
329 | self.postgisdisplaycolumn,
330 | self.extra_expr_columns,
331 | self.postgisschema,
332 | self.postgistable,
333 | self.limit_results,
334 | )
335 | self.schedule_search(query_text, None)
336 |
337 | def do_db_operations(self):
338 | if self.next_query_time is not None and self.next_query_time < time.time():
339 | # It's time to run a query
340 | self.next_query_time = None # Prevent this query from being repeated
341 | self.last_query_time = time.time()
342 | self.perform_search()
343 | else:
344 | # We're not performing a query, close the db connection if it's been open for > 60s
345 | if time.time() > self.last_query_time + self.db_idle_time:
346 | self.db_conn = None
347 |
348 | def perform_search(self):
349 | db = self.get_db()
350 | if db is None and self.data_type != "gpkg":
351 | return
352 |
353 | self.search_results = []
354 | suggestions = []
355 | if self.data_type == "postgres":
356 | cur = db.cursor()
357 | try:
358 | cur.execute(self.query_sql, self.query_dict)
359 | except psycopg2.Error as e:
360 | err_info = "Failed to execute the search query. Please, check your settings. Error message:\n\n"
361 | err_info += "{}".format(e.pgerror)
362 | QMessageBox.critical(None, "Discovery", err_info)
363 | return
364 | result_set = cur.fetchall()
365 | elif self.data_type == "mssql":
366 | result_set = mssql_utils.execute(db, self.query_sql)
367 | elif self.data_type == "oracle":
368 | result_set = oracle_utils.execute(db, self.query_sql)
369 | elif self.data_type == "gpkg":
370 | result_set = gpkg_utils.search_gpkg(*self.query_sql)
371 |
372 | for row in result_set:
373 | geom, epsg, suggestion_text = row[0], row[1], row[2]
374 | extra_data = {}
375 | for idx, extra_col in enumerate(self.extra_expr_columns):
376 | extra_data[extra_col] = row[3 + idx]
377 | self.search_results.append((geom, epsg, suggestion_text, extra_data))
378 | suggestions.append(suggestion_text)
379 | model = self.completer.model()
380 | model.setStringList(suggestions)
381 | self.completer.complete()
382 |
383 | def schedule_search(self, query_text, query_dict):
384 | # Update the search text and the time after which the query should be executed
385 | self.query_sql = query_text
386 | self.query_dict = query_dict
387 | self.next_query_time = time.time() + self.search_delay
388 |
389 | def show_bar_info(self, info_text):
390 | """Optional show info bar message with selected result information"""
391 | self.iface.messageBar().clearWidgets()
392 | if self.bar_info_time:
393 | self.iface.messageBar().pushMessage("Discovery", info_text, level=Qgis.Info, duration=self.bar_info_time)
394 |
395 | def on_result_selected(self, result_index):
396 | # What to do when the user makes a selection
397 | self.select_result(self.search_results[result_index.row()])
398 |
399 | def select_result(self, result_data):
400 | geometry_text, src_epsg, suggestion_text, extra_data = result_data
401 | location_geom = QgsGeometry.fromWkt(geometry_text)
402 | location_geom_type = location_geom.type()
403 | if location_geom_type in {QgsWkbTypes.UnknownGeometry, QgsWkbTypes.NullGeometry}:
404 | # Unknown geometry or no geometry at all
405 | pass
406 | else:
407 | canvas = self.iface.mapCanvas()
408 | dst_srid = canvas.mapSettings().destinationCrs().authid()
409 | transform = QgsCoordinateTransform(
410 | QgsCoordinateReferenceSystem.fromEpsgId(int(src_epsg)),
411 | QgsCoordinateReferenceSystem(dst_srid),
412 | canvas.mapSettings().transformContext(),
413 | )
414 | # Ensure the geometry from the DB is reprojected to the same SRID as the map canvas
415 | location_geom.transform(transform)
416 | location_centroid = location_geom.centroid().asPoint()
417 |
418 | # show temporary marker
419 | if location_geom_type == QgsWkbTypes.PointGeometry:
420 | self.show_marker(location_centroid)
421 | elif location_geom_type == QgsWkbTypes.LineGeometry or location_geom_type == QgsWkbTypes.PolygonGeometry:
422 | self.show_line_rubber_band(location_geom)
423 | else:
424 | # unsupported geometry type
425 | pass
426 |
427 | # Adjust map canvas extent
428 | zoom_method = "Move and Zoom"
429 | if zoom_method == "Move and Zoom":
430 | # with higher priority try to use exact bounding box to zoom to features (if provided)
431 | bbox_str = eval_expression(self.bbox_expr, extra_data)
432 | rect = bbox_str_to_rectangle(bbox_str)
433 | if rect is not None:
434 | # transform the rectangle in case of OTF projection
435 | rect = transform.transformBoundingBox(rect)
436 | else:
437 | # bbox is not available - so let's just use defined scale
438 | # compute target scale. If the result is 2000 this means the target scale is 1:2000
439 | rect = location_geom.boundingBox()
440 | if rect.isEmpty():
441 | scale_denom = eval_expression(self.scale_expr, extra_data, default=2000.0)
442 | rect = canvas.mapSettings().extent()
443 | rect.scale(scale_denom / canvas.scale(), location_centroid)
444 | else:
445 | # enlarge geom bbox to have some margin
446 | rect.scale(1.2)
447 | canvas.setExtent(rect)
448 | elif zoom_method == "Move":
449 | current_extent = QgsGeometry.fromRect(self.iface.mapCanvas().extent())
450 | dx = location_centroid.x() - location_centroid.x()
451 | dy = location_centroid.y() - location_centroid.y()
452 | current_extent.translate(dx, dy)
453 | canvas.setExtent(current_extent.boundingBox())
454 | canvas.refresh()
455 | self.line_edit_timer.start(0)
456 | if self.info_to_clipboard:
457 | QApplication.clipboard().setText(suggestion_text)
458 | suggestion_text += " (copied to clipboard)"
459 | self.show_bar_info(suggestion_text)
460 |
461 | def on_result_highlighted(self, result_idx):
462 | self.line_edit_timer.start(0)
463 |
464 | def reset_line_edit_after_move(self):
465 | self.search_line_edit.setText(self.query_text)
466 |
467 | def get_db(self):
468 | # Create a new connection if required
469 | QApplication.setOverrideCursor(Qt.WaitCursor)
470 | if self.db_conn is None:
471 | if self.data_type == "postgres":
472 | try:
473 | self.db_conn = dbutils.get_connection(self.conn_info)
474 | except psycopg2.Error as e:
475 | err_info = "Failed to connect to the server. Error message:\n\n"
476 | err_info += f"{e.pgerror} - {e}"
477 | QMessageBox.critical(None, "Discovery", err_info)
478 | QApplication.restoreOverrideCursor()
479 | return
480 | elif self.data_type == "mssql":
481 | self.db_conn = mssql_utils.get_mssql_conn(self.conn_info)
482 | elif self.data_type == "oracle":
483 | self.db_conn = oracle_utils.get_oracle_conn(self.conn_info)
484 | QApplication.restoreOverrideCursor()
485 | return self.db_conn
486 |
487 | def change_configuration(self):
488 | self.search_line_edit.setText("")
489 | self.line_edit_timer.start(0)
490 | self.read_config(self.config_combo.currentText())
491 |
492 | def read_config(self, key=""):
493 | # the following code reads the configuration file which setups the plugin to search in the correct database,
494 | # table and method
495 |
496 | settings = QgsSettings()
497 | settings.beginGroup("/Discovery")
498 |
499 | connection = settings.value(key + "connection", "", type=str)
500 | self.data_type = settings.value(key + "data_type", "", type=str)
501 | self.file = settings.value(key + "file", "", type=str)
502 | self.postgisschema = settings.value(key + "schema", "", type=str)
503 | self.postgistable = settings.value(key + "table", "", type=str)
504 | self.postgissearchcolumn = settings.value(key + "search_column", "", type=str)
505 | self.escapespecchars = settings.value(key + "escape_spec_chars", False, type=bool)
506 | self.echosearchcolumn = settings.value(key + "echo_search_column", True, type=bool)
507 | self.postgisdisplaycolumn = settings.value(key + "display_columns", "", type=str)
508 | self.postgisgeomcolumn = settings.value(key + "geom_column", "", type=str)
509 | if settings.value("marker_time_enabled", True, type=bool):
510 | self.display_time = settings.value("marker_time", 5000, type=int)
511 | else:
512 | self.display_time = -1
513 | if settings.value("bar_info_time_enabled", True, type=bool):
514 | self.bar_info_time = settings.value("bar_info_time", 30, type=int)
515 | else:
516 | self.bar_info_time = 0
517 | self.limit_results = settings.value(key + "limit_results", 1000, type=int)
518 | self.info_to_clipboard = settings.value("info_to_clipboard", True, type=bool)
519 |
520 | scale_expr = settings.value(key + "scale_expr", "", type=str)
521 | bbox_expr = settings.value(key + "bbox_expr", "", type=str)
522 |
523 | m_color = QColor()
524 | m_color_name = settings.value(key + "highlight_color", "#e21b1c", type=str)
525 | m_color.setNamedColor(m_color_name)
526 | self.marker.setColor(m_color)
527 | self.rubber_band.setStrokeColor(m_color)
528 | f_color = m_color
529 | f_color.setAlpha(63)
530 | self.rubber_band.setFillColor(f_color)
531 |
532 | if self.is_displayed:
533 | self.hide_marker()
534 | self.hide_rubber_band()
535 | self.is_displayed = False
536 |
537 | self.make_enabled(False) # assume the config is invalid first
538 |
539 | self.db_conn = None
540 | if self.data_type == "postgres":
541 | self.conn_info = dbutils.get_postgres_conn_info(connection)
542 | self.layer = None
543 |
544 | if (
545 | len(connection) == 0
546 | or len(self.postgisschema) == 0
547 | or len(self.postgistable) == 0
548 | or len(self.postgissearchcolumn) == 0
549 | or len(self.postgisgeomcolumn) == 0
550 | ):
551 | return
552 |
553 | if len(self.conn_info) == 0:
554 | iface.messageBar().pushMessage(
555 | "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical
556 | )
557 | return
558 | elif self.data_type == "mssql":
559 | self.conn_info = mssql_utils.get_mssql_conn_info(connection)
560 | self.layer = None
561 |
562 | if (
563 | len(connection) == 0
564 | or len(self.postgisschema) == 0
565 | or len(self.postgistable) == 0
566 | or len(self.postgissearchcolumn) == 0
567 | or len(self.postgisgeomcolumn) == 0
568 | ):
569 | return
570 |
571 | if len(self.conn_info) == 0:
572 | iface.messageBar().pushMessage(
573 | "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical
574 | )
575 | return
576 | elif self.data_type == "oracle":
577 | self.conn_info = oracle_utils.get_oracle_conn_info(connection)
578 | self.layer = None
579 |
580 | if (
581 | len(connection) == 0
582 | or len(self.postgisschema) == 0
583 | or len(self.postgistable) == 0
584 | or len(self.postgissearchcolumn) == 0
585 | or len(self.postgisgeomcolumn) == 0
586 | ):
587 | return
588 |
589 | if len(self.conn_info) == 0:
590 | iface.messageBar().pushMessage(
591 | "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical
592 | )
593 | return
594 | elif self.data_type == "gpkg":
595 | self.layer = QgsVectorLayer(self.file + "|layername=" + self.postgistable, self.postgistable, "ogr")
596 | self.conn_info = None
597 | self.extra_expr_columns = []
598 | self.scale_expr = None
599 | self.bbox_expr = None
600 |
601 | self.make_enabled(True)
602 |
603 | # optional scale expression when zooming in to results
604 | if len(scale_expr) != 0:
605 | expr = QgsExpression(scale_expr)
606 | if expr.hasParserError():
607 | iface.messageBar().pushMessage(
608 | "Discovery", "Invalid scale expression: " + expr.parserErrorString(), level=Qgis.Warning
609 | )
610 | else:
611 | self.scale_expr = scale_expr
612 | self.extra_expr_columns += expr.referencedColumns()
613 |
614 | # optional bbox expression when zooming in to results
615 | if len(bbox_expr) != 0:
616 | expr = QgsExpression(bbox_expr)
617 | if expr.hasParserError():
618 | iface.messageBar().pushMessage(
619 | "Discovery", "Invalid bbox expression: " + expr.parserErrorString(), level=Qgis.Warning
620 | )
621 | else:
622 | self.bbox_expr = bbox_expr
623 | self.extra_expr_columns += expr.referencedColumns()
624 |
625 | def show_config_dialog(self):
626 | dlg = config_dialog.ConfigDialog()
627 | if self.config_combo.currentIndex() >= 0:
628 | dlg.configOptions.setCurrentIndex(self.config_combo.currentIndex())
629 |
630 | if dlg.exec_():
631 | dlg.write_config()
632 | self.config_combo.clear()
633 | for key in [dlg.configOptions.itemText(i) for i in range(dlg.configOptions.count())]:
634 | self.config_combo.addItem(key)
635 |
636 | self.config_combo.setCurrentIndex(dlg.configOptions.currentIndex())
637 | self.change_configuration()
638 |
639 | def make_enabled(self, enabled):
640 | self.search_line_edit.setEnabled(enabled)
641 | self.search_line_edit.setPlaceholderText("Search for..." if enabled else "Search disabled: check configuration")
642 |
643 | def show_marker(self, point):
644 | for m in [self.marker, self.marker2]:
645 | m.setCenter(point)
646 | m.setOpacity(1.0)
647 | m.setVisible(True)
648 | if self.display_time == -1:
649 | self.is_displayed = True
650 | else:
651 | QTimer.singleShot(self.display_time, self.hide_marker)
652 |
653 | def hide_marker(self):
654 | opacity = self.marker.opacity()
655 | if opacity > 0.0:
656 | # produce a fade out effect
657 | opacity -= 0.1
658 | self.marker.setOpacity(opacity)
659 | self.marker2.setOpacity(opacity)
660 | QTimer.singleShot(100, self.hide_marker)
661 | else:
662 | self.marker.setVisible(False)
663 | self.marker2.setVisible(False)
664 |
665 | def show_line_rubber_band(self, geom):
666 | self.rubber_band.reset(geom.type())
667 | self.rubber_band.setToGeometry(geom, None)
668 | self.rubber_band.setVisible(True)
669 | self.rubber_band.setOpacity(1.0)
670 | self.rubber_band.show()
671 | if self.display_time == -1:
672 | self.is_displayed = True
673 | else:
674 | QTimer.singleShot(self.display_time, self.hide_rubber_band)
675 | pass
676 |
677 | def hide_rubber_band(self):
678 | opacity = self.rubber_band.opacity()
679 | if opacity > 0.0:
680 | # produce a fade out effect
681 | opacity -= 0.1
682 | self.rubber_band.setOpacity(opacity)
683 | QTimer.singleShot(100, self.hide_rubber_band)
684 | else:
685 | self.rubber_band.setVisible(False)
686 | self.rubber_band.hide()
687 |
--------------------------------------------------------------------------------
/Discovery/discovery_ts.pro.qtds:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | EnvironmentId
7 | {f8846bc4-233b-4f0d-99a5-65c8e21a01d0}
8 |
9 |
10 | ProjectExplorer.Project.ActiveTarget
11 | 1
12 |
13 |
14 | ProjectExplorer.Project.EditorSettings
15 |
16 | true
17 | false
18 | true
19 |
20 | Cpp
21 |
22 | CppGlobal
23 |
24 |
25 |
26 | QmlJS
27 |
28 | QmlJSGlobal
29 |
30 |
31 | 2
32 | UTF-8
33 | false
34 | 4
35 | false
36 | 80
37 | true
38 | true
39 | 1
40 | false
41 | true
42 | false
43 | 0
44 | true
45 | true
46 | 0
47 | 8
48 | true
49 | false
50 | 1
51 | true
52 | true
53 | true
54 | *.md, *.MD, Makefile
55 | false
56 | true
57 |
58 |
59 |
60 | ProjectExplorer.Project.Target.0
61 |
62 | Desktop
63 | Desktop Qt 5.15.5
64 | Desktop Qt 5.15.5
65 | {8994bd34-5ed9-4c45-8c0a-94c8f33eca4a}
66 | 0
67 | 0
68 | 0
69 |
70 | 0
71 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_5_15_5-Debug
72 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_5_15_5-Debug
73 |
74 |
75 | true
76 | QtProjectManager.QMakeBuildStep
77 | false
78 |
79 |
80 |
81 | true
82 | Qt4ProjectManager.MakeStep
83 |
84 | 2
85 | Build
86 | Build
87 | ProjectExplorer.BuildSteps.Build
88 |
89 |
90 |
91 | true
92 | Qt4ProjectManager.MakeStep
93 | clean
94 |
95 | 1
96 | Clean
97 | Clean
98 | ProjectExplorer.BuildSteps.Clean
99 |
100 | 2
101 | false
102 |
103 | false
104 |
105 | Debug
106 | Qt4ProjectManager.Qt4BuildConfiguration
107 | 2
108 |
109 |
110 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_5_15_5-Release
111 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_5_15_5-Release
112 |
113 |
114 | true
115 | QtProjectManager.QMakeBuildStep
116 | false
117 |
118 |
119 |
120 | true
121 | Qt4ProjectManager.MakeStep
122 |
123 | 2
124 | Build
125 | Build
126 | ProjectExplorer.BuildSteps.Build
127 |
128 |
129 |
130 | true
131 | Qt4ProjectManager.MakeStep
132 | clean
133 |
134 | 1
135 | Clean
136 | Clean
137 | ProjectExplorer.BuildSteps.Clean
138 |
139 | 2
140 | false
141 |
142 | false
143 |
144 | Release
145 | Qt4ProjectManager.Qt4BuildConfiguration
146 | 0
147 | 0
148 |
149 |
150 | 0
151 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_5_15_5-Profile
152 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_5_15_5-Profile
153 |
154 |
155 | true
156 | QtProjectManager.QMakeBuildStep
157 | false
158 |
159 |
160 |
161 | true
162 | Qt4ProjectManager.MakeStep
163 |
164 | 2
165 | Build
166 | Build
167 | ProjectExplorer.BuildSteps.Build
168 |
169 |
170 |
171 | true
172 | Qt4ProjectManager.MakeStep
173 | clean
174 |
175 | 1
176 | Clean
177 | Clean
178 | ProjectExplorer.BuildSteps.Clean
179 |
180 | 2
181 | false
182 |
183 | false
184 |
185 | Profile
186 | Qt4ProjectManager.Qt4BuildConfiguration
187 | 0
188 | 0
189 | 0
190 |
191 | 3
192 |
193 |
194 | 0
195 | Deploy
196 | Deploy
197 | ProjectExplorer.BuildSteps.Deploy
198 |
199 | 1
200 |
201 | false
202 | ProjectExplorer.DefaultDeployConfiguration
203 |
204 | 1
205 |
206 | true
207 |
208 | 2
209 |
210 | ProjectExplorer.CustomExecutableRunConfiguration
211 |
212 | false
213 | true
214 | false
215 | true
216 |
217 | 1
218 |
219 |
220 |
221 | ProjectExplorer.Project.Target.1
222 |
223 | Desktop
224 | Desktop Qt 6.3.1
225 | Desktop Qt 6.3.1
226 | {63f87550-2541-4163-9631-08b7fea781da}
227 | 0
228 | 0
229 | 0
230 |
231 | 0
232 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_6_3_1-Debug
233 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_6_3_1-Debug
234 |
235 |
236 | true
237 | QtProjectManager.QMakeBuildStep
238 | false
239 |
240 |
241 |
242 | true
243 | Qt4ProjectManager.MakeStep
244 |
245 | 2
246 | Build
247 | Build
248 | ProjectExplorer.BuildSteps.Build
249 |
250 |
251 |
252 | true
253 | Qt4ProjectManager.MakeStep
254 | clean
255 |
256 | 1
257 | Clean
258 | Clean
259 | ProjectExplorer.BuildSteps.Clean
260 |
261 | 2
262 | false
263 |
264 | false
265 |
266 | Debug
267 | Qt4ProjectManager.Qt4BuildConfiguration
268 | 2
269 |
270 |
271 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_6_3_1-Release
272 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_6_3_1-Release
273 |
274 |
275 | true
276 | QtProjectManager.QMakeBuildStep
277 | false
278 |
279 |
280 |
281 | true
282 | Qt4ProjectManager.MakeStep
283 |
284 | 2
285 | Build
286 | Build
287 | ProjectExplorer.BuildSteps.Build
288 |
289 |
290 |
291 | true
292 | Qt4ProjectManager.MakeStep
293 | clean
294 |
295 | 1
296 | Clean
297 | Clean
298 | ProjectExplorer.BuildSteps.Clean
299 |
300 | 2
301 | false
302 |
303 | false
304 |
305 | Release
306 | Qt4ProjectManager.Qt4BuildConfiguration
307 | 0
308 | 0
309 |
310 |
311 | 0
312 | C:\Users\ryu\Documents\GitHub\qgis-discovery-plugin\build-discovery_ts-Desktop_Qt_6_3_1-Profile
313 | C:/Users/ryu/Documents/GitHub/qgis-discovery-plugin/build-discovery_ts-Desktop_Qt_6_3_1-Profile
314 |
315 |
316 | true
317 | QtProjectManager.QMakeBuildStep
318 | false
319 |
320 |
321 |
322 | true
323 | Qt4ProjectManager.MakeStep
324 |
325 | 2
326 | Build
327 | Build
328 | ProjectExplorer.BuildSteps.Build
329 |
330 |
331 |
332 | true
333 | Qt4ProjectManager.MakeStep
334 | clean
335 |
336 | 1
337 | Clean
338 | Clean
339 | ProjectExplorer.BuildSteps.Clean
340 |
341 | 2
342 | false
343 |
344 | false
345 |
346 | Profile
347 | Qt4ProjectManager.Qt4BuildConfiguration
348 | 0
349 | 0
350 | 0
351 |
352 | 3
353 |
354 |
355 | 0
356 | Deploy
357 | Deploy
358 | ProjectExplorer.BuildSteps.Deploy
359 |
360 | 1
361 |
362 | false
363 | ProjectExplorer.DefaultDeployConfiguration
364 |
365 | 1
366 |
367 | true
368 |
369 | 2
370 |
371 | ProjectExplorer.CustomExecutableRunConfiguration
372 |
373 | false
374 | true
375 | false
376 | true
377 |
378 | 1
379 |
380 |
381 |
382 | ProjectExplorer.Project.TargetCount
383 | 2
384 |
385 |
386 | ProjectExplorer.Project.Updater.FileVersion
387 | 22
388 |
389 |
390 | Version
391 | 22
392 |
393 |
394 |
--------------------------------------------------------------------------------