db_model.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. """
  2. /***************************************************************************
  3. Name : DB Manager
  4. Description : Database manager plugin for QGIS
  5. Date : May 23, 2011
  6. copyright : (C) 2011 by Giuseppe Sucameli
  7. email : brush.tyler@gmail.com
  8. ***************************************************************************/
  9. /***************************************************************************
  10. * *
  11. * This program is free software; you can redistribute it and/or modify *
  12. * it under the terms of the GNU General Public License as published by *
  13. * the Free Software Foundation; either version 2 of the License, or *
  14. * (at your option) any later version. *
  15. * *
  16. ***************************************************************************/
  17. """
  18. from functools import partial
  19. from qgis.PyQt.QtCore import Qt, QObject, qDebug, QByteArray, QMimeData, QDataStream, QIODevice, QFileInfo, QAbstractItemModel, QModelIndex, pyqtSignal
  20. from qgis.PyQt.QtWidgets import QApplication, QMessageBox
  21. from qgis.PyQt.QtGui import QIcon
  22. from .db_plugins import supportedDbTypes, createDbPlugin
  23. from .db_plugins.plugin import BaseError, Table, Database
  24. from .dlg_db_error import DlgDbError
  25. from qgis.core import (
  26. QgsApplication,
  27. QgsDataSourceUri,
  28. QgsVectorLayer,
  29. QgsRasterLayer,
  30. QgsMimeDataUtils,
  31. QgsProviderConnectionException,
  32. QgsProviderRegistry,
  33. QgsAbstractDatabaseProviderConnection,
  34. QgsMessageLog,
  35. )
  36. from qgis.utils import OverrideCursor
  37. from . import resources_rc # NOQA
  38. try:
  39. from qgis.core import QgsVectorLayerExporter # NOQA
  40. isImportVectorAvail = True
  41. except:
  42. isImportVectorAvail = False
  43. class TreeItem(QObject):
  44. deleted = pyqtSignal()
  45. changed = pyqtSignal()
  46. def __init__(self, data, parent=None):
  47. QObject.__init__(self, parent)
  48. self.populated = False
  49. self.itemData = data
  50. self.childItems = []
  51. if parent:
  52. parent.appendChild(self)
  53. def childRemoved(self):
  54. self.itemChanged()
  55. def itemChanged(self):
  56. self.changed.emit()
  57. def itemDeleted(self):
  58. self.deleted.emit()
  59. def populate(self):
  60. self.populated = True
  61. return True
  62. def getItemData(self):
  63. return self.itemData
  64. def appendChild(self, child):
  65. self.childItems.append(child)
  66. child.deleted.connect(self.childRemoved)
  67. def child(self, row):
  68. return self.childItems[row]
  69. def removeChild(self, row):
  70. if row >= 0 and row < len(self.childItems):
  71. self.childItems[row].itemData.deleteLater()
  72. self.childItems[row].deleted.disconnect(self.childRemoved)
  73. del self.childItems[row]
  74. def childCount(self):
  75. return len(self.childItems)
  76. def columnCount(self):
  77. return 1
  78. def row(self):
  79. if self.parent():
  80. for row, item in enumerate(self.parent().childItems):
  81. if item is self:
  82. return row
  83. return 0
  84. def data(self, column):
  85. return "" if column == 0 else None
  86. def icon(self):
  87. return None
  88. def path(self):
  89. pathList = []
  90. if self.parent():
  91. pathList.extend(self.parent().path())
  92. pathList.append(self.data(0))
  93. return pathList
  94. class PluginItem(TreeItem):
  95. def __init__(self, dbplugin, parent=None):
  96. TreeItem.__init__(self, dbplugin, parent)
  97. def populate(self):
  98. if self.populated:
  99. return True
  100. # create items for connections
  101. for c in self.getItemData().connections():
  102. ConnectionItem(c, self)
  103. self.populated = True
  104. return True
  105. def data(self, column):
  106. if column == 0:
  107. return self.getItemData().typeNameString()
  108. return None
  109. def icon(self):
  110. return self.getItemData().icon()
  111. def path(self):
  112. return [self.getItemData().typeName()]
  113. class ConnectionItem(TreeItem):
  114. def __init__(self, connection, parent=None):
  115. TreeItem.__init__(self, connection, parent)
  116. connection.changed.connect(self.itemChanged)
  117. connection.deleted.connect(self.itemDeleted)
  118. # load (shared) icon with first instance of table item
  119. if not hasattr(ConnectionItem, 'connectedIcon'):
  120. ConnectionItem.connectedIcon = QIcon(":/db_manager/icons/plugged.png")
  121. ConnectionItem.disconnectedIcon = QIcon(":/db_manager/icons/unplugged.png")
  122. def data(self, column):
  123. if column == 0:
  124. return self.getItemData().connectionName()
  125. return None
  126. def icon(self):
  127. return self.getItemData().connectionIcon()
  128. def populate(self):
  129. if self.populated:
  130. return True
  131. connection = self.getItemData()
  132. if connection.database() is None:
  133. # connect to database
  134. try:
  135. if not connection.connect():
  136. return False
  137. except BaseError as e:
  138. DlgDbError.showError(e, None)
  139. return False
  140. database = connection.database()
  141. database.changed.connect(self.itemChanged)
  142. database.deleted.connect(self.itemDeleted)
  143. schemas = database.schemas()
  144. if schemas is not None:
  145. for s in schemas:
  146. SchemaItem(s, self)
  147. else:
  148. tables = database.tables()
  149. for t in tables:
  150. TableItem(t, self)
  151. self.populated = True
  152. return True
  153. def isConnected(self):
  154. return self.getItemData().database() is not None
  155. # def icon(self):
  156. # return self.connectedIcon if self.isConnected() else self.disconnectedIcon
  157. class SchemaItem(TreeItem):
  158. def __init__(self, schema, parent):
  159. TreeItem.__init__(self, schema, parent)
  160. schema.changed.connect(self.itemChanged)
  161. schema.deleted.connect(self.itemDeleted)
  162. # load (shared) icon with first instance of schema item
  163. if not hasattr(SchemaItem, 'schemaIcon'):
  164. SchemaItem.schemaIcon = QIcon(":/db_manager/icons/namespace.png")
  165. def data(self, column):
  166. if column == 0:
  167. return self.getItemData().name
  168. return None
  169. def icon(self):
  170. return self.schemaIcon
  171. def populate(self):
  172. if self.populated:
  173. return True
  174. for t in self.getItemData().tables():
  175. TableItem(t, self)
  176. self.populated = True
  177. return True
  178. class TableItem(TreeItem):
  179. def __init__(self, table, parent):
  180. TreeItem.__init__(self, table, parent)
  181. table.changed.connect(self.itemChanged)
  182. table.deleted.connect(self.itemDeleted)
  183. self.populate()
  184. # load (shared) icon with first instance of table item
  185. if not hasattr(TableItem, 'tableIcon'):
  186. TableItem.tableIcon = QgsApplication.getThemeIcon("/mIconTableLayer.svg")
  187. TableItem.viewIcon = QIcon(":/db_manager/icons/view.png")
  188. TableItem.viewMaterializedIcon = QIcon(":/db_manager/icons/view_materialized.png")
  189. TableItem.layerPointIcon = QgsApplication.getThemeIcon("/mIconPointLayer.svg")
  190. TableItem.layerLineIcon = QgsApplication.getThemeIcon("/mIconLineLayer.svg")
  191. TableItem.layerPolygonIcon = QgsApplication.getThemeIcon("/mIconPolygonLayer.svg")
  192. TableItem.layerRasterIcon = QgsApplication.getThemeIcon("/mIconRasterLayer.svg")
  193. TableItem.layerUnknownIcon = QIcon(":/db_manager/icons/layer_unknown.png")
  194. def data(self, column):
  195. if column == 0:
  196. return self.getItemData().name
  197. elif column == 1:
  198. if self.getItemData().type == Table.VectorType:
  199. return self.getItemData().geomType
  200. return None
  201. def icon(self):
  202. if self.getItemData().type == Table.VectorType:
  203. geom_type = self.getItemData().geomType
  204. if geom_type is not None:
  205. if geom_type.find('POINT') != -1:
  206. return self.layerPointIcon
  207. elif geom_type.find('LINESTRING') != -1 or geom_type in ('CIRCULARSTRING', 'COMPOUNDCURVE', 'MULTICURVE'):
  208. return self.layerLineIcon
  209. elif geom_type.find('POLYGON') != -1 or geom_type == 'MULTISURFACE':
  210. return self.layerPolygonIcon
  211. return self.layerUnknownIcon
  212. elif self.getItemData().type == Table.RasterType:
  213. return self.layerRasterIcon
  214. if self.getItemData().isView:
  215. if hasattr(self.getItemData(), '_relationType') and self.getItemData()._relationType == 'm':
  216. return self.viewMaterializedIcon
  217. else:
  218. return self.viewIcon
  219. return self.tableIcon
  220. def path(self):
  221. pathList = []
  222. if self.parent():
  223. pathList.extend(self.parent().path())
  224. if self.getItemData().type == Table.VectorType:
  225. pathList.append("%s::%s" % (self.data(0), self.getItemData().geomColumn))
  226. else:
  227. pathList.append(self.data(0))
  228. return pathList
  229. class DBModel(QAbstractItemModel):
  230. importVector = pyqtSignal(QgsVectorLayer, Database, QgsDataSourceUri, QModelIndex)
  231. notPopulated = pyqtSignal(QModelIndex)
  232. def __init__(self, parent=None):
  233. global isImportVectorAvail
  234. QAbstractItemModel.__init__(self, parent)
  235. self.treeView = parent
  236. self.header = [self.tr('Databases')]
  237. if isImportVectorAvail:
  238. self.importVector.connect(self.vectorImport)
  239. self.hasSpatialiteSupport = "spatialite" in supportedDbTypes()
  240. self.hasGPKGSupport = "gpkg" in supportedDbTypes()
  241. self.rootItem = TreeItem(None, None)
  242. for dbtype in supportedDbTypes():
  243. dbpluginclass = createDbPlugin(dbtype)
  244. item = PluginItem(dbpluginclass, self.rootItem)
  245. item.changed.connect(partial(self.refreshItem, item))
  246. def refreshItem(self, item):
  247. if isinstance(item, TreeItem):
  248. # find the index for the tree item using the path
  249. index = self._rPath2Index(item.path())
  250. else:
  251. # find the index for the db item
  252. index = self._rItem2Index(item)
  253. if index.isValid():
  254. self._refreshIndex(index)
  255. else:
  256. qDebug("invalid index")
  257. def _rItem2Index(self, item, parent=None):
  258. if parent is None:
  259. parent = QModelIndex()
  260. if item == self.getItem(parent):
  261. return parent
  262. if not parent.isValid() or parent.internalPointer().populated:
  263. for i in range(self.rowCount(parent)):
  264. index = self.index(i, 0, parent)
  265. index = self._rItem2Index(item, index)
  266. if index.isValid():
  267. return index
  268. return QModelIndex()
  269. def _rPath2Index(self, path, parent=None, n=0):
  270. if parent is None:
  271. parent = QModelIndex()
  272. if path is None or len(path) == 0:
  273. return parent
  274. for i in range(self.rowCount(parent)):
  275. index = self.index(i, 0, parent)
  276. if self._getPath(index)[n] == path[0]:
  277. return self._rPath2Index(path[1:], index, n + 1)
  278. return parent
  279. def getItem(self, index):
  280. if not index.isValid():
  281. return None
  282. return index.internalPointer().getItemData()
  283. def _getPath(self, index):
  284. if not index.isValid():
  285. return None
  286. return index.internalPointer().path()
  287. def columnCount(self, parent):
  288. return 1
  289. def data(self, index, role):
  290. if not index.isValid():
  291. return None
  292. if role == Qt.DecorationRole and index.column() == 0:
  293. icon = index.internalPointer().icon()
  294. if icon:
  295. return icon
  296. if role != Qt.DisplayRole and role != Qt.EditRole:
  297. return None
  298. retval = index.internalPointer().data(index.column())
  299. return retval
  300. def flags(self, index):
  301. global isImportVectorAvail
  302. if not index.isValid():
  303. return Qt.NoItemFlags
  304. flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
  305. if index.column() == 0:
  306. item = index.internalPointer()
  307. if isinstance(item, SchemaItem) or isinstance(item, TableItem):
  308. flags |= Qt.ItemIsEditable
  309. if isinstance(item, TableItem):
  310. flags |= Qt.ItemIsDragEnabled
  311. # vectors/tables can be dropped on connected databases to be imported
  312. if isImportVectorAvail:
  313. if isinstance(item, ConnectionItem) and item.populated:
  314. flags |= Qt.ItemIsDropEnabled
  315. if isinstance(item, (SchemaItem, TableItem)):
  316. flags |= Qt.ItemIsDropEnabled
  317. # SL/Geopackage db files can be dropped everywhere in the tree
  318. if self.hasSpatialiteSupport or self.hasGPKGSupport:
  319. flags |= Qt.ItemIsDropEnabled
  320. return flags
  321. def headerData(self, section, orientation, role):
  322. if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self.header):
  323. return self.header[section]
  324. return None
  325. def index(self, row, column, parent):
  326. if not self.hasIndex(row, column, parent):
  327. return QModelIndex()
  328. parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
  329. childItem = parentItem.child(row)
  330. if childItem:
  331. return self.createIndex(row, column, childItem)
  332. return QModelIndex()
  333. def parent(self, index):
  334. if not index.isValid():
  335. return QModelIndex()
  336. childItem = index.internalPointer()
  337. parentItem = childItem.parent()
  338. if parentItem == self.rootItem:
  339. return QModelIndex()
  340. return self.createIndex(parentItem.row(), 0, parentItem)
  341. def rowCount(self, parent):
  342. parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
  343. if not parentItem.populated:
  344. self._refreshIndex(parent, True)
  345. return parentItem.childCount()
  346. def hasChildren(self, parent):
  347. parentItem = parent.internalPointer() if parent.isValid() else self.rootItem
  348. return parentItem.childCount() > 0 or not parentItem.populated
  349. def setData(self, index, value, role):
  350. if role != Qt.EditRole or index.column() != 0:
  351. return False
  352. item = index.internalPointer()
  353. new_value = str(value)
  354. if isinstance(item, SchemaItem) or isinstance(item, TableItem):
  355. obj = item.getItemData()
  356. # rename schema or table or view
  357. if new_value == obj.name:
  358. return False
  359. with OverrideCursor(Qt.WaitCursor):
  360. try:
  361. obj.rename(new_value)
  362. self._onDataChanged(index)
  363. except BaseError as e:
  364. DlgDbError.showError(e, self.treeView)
  365. return False
  366. else:
  367. return True
  368. return False
  369. def removeRows(self, row, count, parent):
  370. self.beginRemoveRows(parent, row, count + row - 1)
  371. item = parent.internalPointer()
  372. for i in range(row, count + row):
  373. item.removeChild(row)
  374. self.endRemoveRows()
  375. def _refreshIndex(self, index, force=False):
  376. with OverrideCursor(Qt.WaitCursor):
  377. try:
  378. item = index.internalPointer() if index.isValid() else self.rootItem
  379. prevPopulated = item.populated
  380. if prevPopulated:
  381. self.removeRows(0, self.rowCount(index), index)
  382. item.populated = False
  383. if prevPopulated or force:
  384. if item.populate():
  385. for child in item.childItems:
  386. child.changed.connect(partial(self.refreshItem, child))
  387. self._onDataChanged(index)
  388. else:
  389. self.notPopulated.emit(index)
  390. except BaseError:
  391. item.populated = False
  392. def _onDataChanged(self, indexFrom, indexTo=None):
  393. if indexTo is None:
  394. indexTo = indexFrom
  395. self.dataChanged.emit(indexFrom, indexTo)
  396. QGIS_URI_MIME = "application/x-vnd.qgis.qgis.uri"
  397. def mimeTypes(self):
  398. return ["text/uri-list", self.QGIS_URI_MIME]
  399. def mimeData(self, indexes):
  400. mimeData = QMimeData()
  401. encodedData = QByteArray()
  402. stream = QDataStream(encodedData, QIODevice.WriteOnly)
  403. for index in indexes:
  404. if not index.isValid():
  405. continue
  406. if not isinstance(index.internalPointer(), TableItem):
  407. continue
  408. table = self.getItem(index)
  409. stream.writeQString(table.mimeUri())
  410. mimeData.setData(self.QGIS_URI_MIME, encodedData)
  411. return mimeData
  412. def dropMimeData(self, data, action, row, column, parent):
  413. global isImportVectorAvail
  414. if action == Qt.IgnoreAction:
  415. return True
  416. # vectors/tables to be imported must be dropped on connected db, schema or table
  417. canImportLayer = isImportVectorAvail and parent.isValid() and \
  418. (isinstance(parent.internalPointer(), (SchemaItem, TableItem)) or
  419. (isinstance(parent.internalPointer(), ConnectionItem) and parent.internalPointer().populated))
  420. added = 0
  421. if data.hasUrls():
  422. for u in data.urls():
  423. filename = u.toLocalFile()
  424. if filename == "":
  425. continue
  426. if self.hasSpatialiteSupport:
  427. from .db_plugins.spatialite.connector import SpatiaLiteDBConnector
  428. if SpatiaLiteDBConnector.isValidDatabase(filename):
  429. # retrieve the SL plugin tree item using its path
  430. index = self._rPath2Index(["spatialite"])
  431. if not index.isValid():
  432. continue
  433. item = index.internalPointer()
  434. conn_name = QFileInfo(filename).fileName()
  435. uri = QgsDataSourceUri()
  436. uri.setDatabase(filename)
  437. item.getItemData().addConnection(conn_name, uri)
  438. item.changed.emit()
  439. added += 1
  440. continue
  441. if canImportLayer:
  442. if QgsRasterLayer.isValidRasterFileName(filename):
  443. layerType = 'raster'
  444. providerKey = 'gdal'
  445. else:
  446. layerType = 'vector'
  447. providerKey = 'ogr'
  448. layerName = QFileInfo(filename).completeBaseName()
  449. if self.importLayer(layerType, providerKey, layerName, filename, parent):
  450. added += 1
  451. if data.hasFormat(self.QGIS_URI_MIME):
  452. for uri in QgsMimeDataUtils.decodeUriList(data):
  453. if canImportLayer:
  454. if self.importLayer(uri.layerType, uri.providerKey, uri.name, uri.uri, parent):
  455. added += 1
  456. return added > 0
  457. def importLayer(self, layerType, providerKey, layerName, uriString, parent):
  458. global isImportVectorAvail
  459. if not isImportVectorAvail:
  460. return False
  461. if layerType == 'raster':
  462. return False # not implemented yet
  463. inLayer = QgsRasterLayer(uriString, layerName, providerKey)
  464. else:
  465. inLayer = QgsVectorLayer(uriString, layerName, providerKey)
  466. if not inLayer.isValid():
  467. # invalid layer
  468. QMessageBox.warning(None, self.tr("Invalid layer"), self.tr("Unable to load the layer {0}").format(inLayer.name()))
  469. return False
  470. # retrieve information about the new table's db and schema
  471. outItem = parent.internalPointer()
  472. outObj = outItem.getItemData()
  473. outDb = outObj.database()
  474. outSchema = None
  475. if isinstance(outItem, SchemaItem):
  476. outSchema = outObj
  477. elif isinstance(outItem, TableItem):
  478. outSchema = outObj.schema()
  479. # toIndex will point to the parent item of the new table
  480. toIndex = parent
  481. if isinstance(toIndex.internalPointer(), TableItem):
  482. toIndex = toIndex.parent()
  483. if inLayer.type() == inLayer.VectorLayer:
  484. # create the output uri
  485. schema = outSchema.name if outDb.schemas() is not None and outSchema is not None else ""
  486. pkCol = geomCol = ""
  487. # default pk and geom field name value
  488. if providerKey in ['postgres', 'spatialite']:
  489. inUri = QgsDataSourceUri(inLayer.source())
  490. pkCol = inUri.keyColumn()
  491. geomCol = inUri.geometryColumn()
  492. outUri = outDb.uri()
  493. outUri.setDataSource(schema, layerName, geomCol, "", pkCol)
  494. self.importVector.emit(inLayer, outDb, outUri, toIndex)
  495. return True
  496. return False
  497. def vectorImport(self, inLayer, outDb, outUri, parent):
  498. global isImportVectorAvail
  499. if not isImportVectorAvail:
  500. return False
  501. try:
  502. from .dlg_import_vector import DlgImportVector
  503. dlg = DlgImportVector(inLayer, outDb, outUri)
  504. QApplication.restoreOverrideCursor()
  505. if dlg.exec_():
  506. self._refreshIndex(parent)
  507. finally:
  508. inLayer.deleteLater()