def initialize(self): ''' Configure the mapper based on the user settings. ''' #Load headers if self._dbModel != None: headers = self._dbModel.displayMapping().values() self._tableModel = BaseSTDMTableModel([], headers, self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode( QHeaderView.Stretch)
def initialize(self): """ Configure the mapper based on the user settings. """ from stdm.data import numeric_varchar_columns #Load headers if not self._dbModel is None: headers = [] display_cols = numeric_varchar_columns(self._ds_name) #Ensure only displayable values are included for c, dc in self._dbModel.displayMapping().iteritems(): if c in display_cols: headers.append(dc) self._tableModel = BaseSTDMTableModel([],headers,self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode(QHeaderView.Interactive) self._tbFKEntity.verticalHeader().setVisible(True) ''' If expression builder is enabled then disable edit button since mapper cannot work in both selection and editing mode. ''' if self._use_expression_builder: self._filter_entity_btn.setVisible(True) self._edit_entity_btn.setVisible(False)
def initialize(self): ''' Configure the mapper based on the user settings. ''' #Load headers if self._dbModel != None: headers = self._dbModel.displayMapping().values() self._tableModel = BaseSTDMTableModel([],headers,self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode(QHeaderView.Stretch)
class ForeignKeyMapper(QWidget): ''' Foreign key mapper widget. ''' #Custom signals beforeEntityAdded = pyqtSignal("PyQt_PyObject") afterEntityAdded = pyqtSignal("PyQt_PyObject") entityRemoved = pyqtSignal("PyQt_PyObject") def __init__(self,parent=None): QWidget.__init__(self,parent) self._tbFKEntity = QTableView(self) self._tbFKEntity.setEditTriggers(QAbstractItemView.NoEditTriggers) self._tbFKEntity.setAlternatingRowColors(True) self._tbFKEntity.setSelectionBehavior(QAbstractItemView.SelectRows) tbActions = QToolBar() tbActions.setIconSize(QSize(16,16)) self._addEntityAction = QAction(QIcon(":/plugins/stdm/images/icons/add.png"), QApplication.translate("ForeignKeyMapper","Add"),self) self.connect(self._addEntityAction,SIGNAL("triggered()"),self.onAddEntity) self._removeEntityAction = QAction(QIcon(":/plugins/stdm/images/icons/remove.png"), QApplication.translate("ForeignKeyMapper","Remove"),self) self.connect(self._removeEntityAction,SIGNAL("triggered()"),self.onRemoveEntity) tbActions.addAction(self._addEntityAction) tbActions.addAction(self._removeEntityAction) layout = QVBoxLayout(self) layout.setSpacing(2) layout.setMargin(5) layout.addWidget(tbActions) layout.addWidget(self._tbFKEntity) #Instance variables self._dbModel = None self._omitPropertyNames = [] self._entitySelector = None self._entitySelectorState = None self._supportsLists = True self._tableModel = None self._notifBar = None self._cellFormatters = {} self._deleteOnRemove = False self._uniqueValueColIndices = OrderedDict() def initialize(self): ''' Configure the mapper based on the user settings. ''' #Load headers if self._dbModel != None: headers = self._dbModel.displayMapping().values() self._tableModel = BaseSTDMTableModel([],headers,self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode(QHeaderView.Stretch) def databaseModel(self): ''' Returns the database model that represents the foreign key entity. ''' return self._dbModel def setDatabaseModel(self,model): ''' Set the database model that represents the foreign key entity. Model has to be a callable. ''' self._dbModel = model def setEmitPropertyNames(self,propnames): ''' Set the property names to be omitted from the display in the table list view. ''' self._omitPropertyNames = propnames def omitPropertyNames(self): ''' Returns the property names to be omitted from the display in the table list view. ''' return self._omitPropertyNames def setCellFormatters(self,formattermapping): ''' Dictionary of attribute mappings and corresponding functions for formatting the attribute value to the display value. ''' self._cellFormatters = formattermapping def addCellFormatter(self,attributeName,formatterFunc): ''' Add a new cell formatter configuration to the collection ''' self._cellFormatters[attributeName] = formatterFunc def cellFormatters(self): """ Returns a dictionary of cell formatters used by the foreign key mapper. """ return self._cellFormatters def entitySelector(self): ''' Returns the dialog for selecting the entity objects. ''' return self._entitySelector def setEntitySelector(self,selector,state=SELECT): ''' Set the dialog for selecting entity objects. Selector must be a callable. ''' self._entitySelector = selector self._entitySelectorState = state def supportList(self): ''' Returns whether the mapper supports only one item or multiple entities i.e. one-to-one and one-to-many mapping. Default is 'True'. ''' return self._supportsLists def setSupportsList(self,supportsList): ''' Sets whether the mapper supports only one item or multiple entities i.e. one-to-one (False) and one-to-many mapping (True). ''' self._supportsLists = supportsList def setNotificationBar(self,notificationBar): ''' Set the notification bar for displaying user messages. ''' self._notifBar = notificationBar def viewModel(self): ''' Return the view model used by the table view. ''' return self._tableModel def deleteOnRemove(self): ''' Returns the state whether a record should be deleted from the database when it is removed from the list. ''' return self._deleteOnRemove def setDeleteonRemove(self,delete): ''' Set whether whether a record should be deleted from the database when it is removed from the list. ''' self._deleteOnRemove = delete def addUniqueColumnName(self,colName,replace = True): ''' Set the name of the column whose values are to be unique. If 'replace' is True then the existing row will be replaced with one with the new value; else, the new row will not be added to the list. ''' headers = self._dbModel.displayMapping().values() colIndex = getIndex(headers,colName) if colIndex != -1: self.addUniqueColumnIndex(colIndex, replace) def addUniqueColumnIndex(self,colIndex,replace = True): ''' Set the index of the column whose values are to be unique. The column indices are zero-based. If 'replace' is True then the existing row will be replaced with the new value; else, the new row will not be added to the list. For multiple replace rules defined, then the first one added to the collection is the one that will be applied. ''' self._uniqueValueColIndices[colIndex] = replace def _removeRow(self,rowNumber): ''' Remove the row at the given index. ''' self._tableModel.removeRows(rowNumber, 1) def onRemoveEntity(self): ''' Slot raised on clicking to remove the selected entity. ''' selectedRowIndices = self._tbFKEntity.selectionModel().selectedRows(0) if len(selectedRowIndices) == 0: msg = QApplication.translate("ForeignKeyMapper","Please select the record to be removed.") self._notifBar.clear() self._notifBar.insertWarningNotification(msg) for selectedRowIndex in selectedRowIndices: #Delete record from database if flag has been set to True recId= selectedRowIndex.data() dbHandler = self._dbModel() delRec = dbHandler.queryObject().filter(self._dbModel.id == recId).first() if not delRec is None: self.entityRemoved.emit(delRec) if self._deleteOnRemove: delRec.delete() self._removeRow(selectedRowIndex.row()) def _recordIds(self): ''' Returns the primary keys of the records in the table. ''' rowCount = self._tableModel.rowCount() recordIds = [] for r in range(rowCount): #Get ID value modelIndex = self._tableModel.index(r,0) modelId = modelIndex.data() recordIds.append(modelId) return recordIds def entities(self): ''' Returns the model instance(s) depending on the configuration specified by the user. ''' recIds = self._recordIds() modelInstances = self._modelInstanceFromIds(recIds) if len(modelInstances) == 0: return None else: if self._supportsLists: return modelInstances else: return modelInstances[0] def setEntities(self,entities): ''' Insert entities into the table. ''' if isinstance(entities,list): for entity in entities: self._insertModelToView(entity) else: self._insertModelToView(entities) def searchModel(self,columnIndex,columnValue): ''' Searches for 'columnValue' in the column whose index is specified by 'columnIndex' in all rows contained in the model. ''' if isinstance (columnValue,QVariant): columnValue = str(columnValue.toString()) if not isinstance(columnValue,str): columnValue = str(columnValue) columnValue = columnValue.strip() proxy = QSortFilterProxyModel(self) proxy.setSourceModel(self._tableModel) proxy.setFilterKeyColumn(columnIndex) proxy.setFilterFixedString(columnValue) #Will return model index containing the primary key. matchingIndex = proxy.mapToSource(proxy.index(0,0)) return matchingIndex def _modelInstanceFromIds(self,ids): ''' Returns the model instance based the value of its primary key. ''' dbHandler = self._dbModel() modelInstances = [] for modelId in ids: modelObj = dbHandler.queryObject().filter(self._dbModel.id == modelId).first() if modelObj != None: modelInstances.append(modelObj) return modelInstances def _onRecordSelectedEntityBrowser(self,recid): ''' Slot raised when the user has clicked the select button in the 'EntityBrowser' dialog to add the selected record to the table's list. Add the record to the foreign key table using the mappings. ''' #Check if the record exists using the primary key so as to ensure only one instance is added to the table recIndex = getIndex(self._recordIds(),id) if recIndex != -1: return dbHandler = self._dbModel() modelObj = dbHandler.queryObject().filter(self._dbModel.id == recid).first() if modelObj != None: #Raise before entity added signal self.beforeEntityAdded.emit(modelObj) #Validate for unique value configurations for colIndex,replace in self._uniqueValueColIndices.items(): attrName = self._dbModel.displayMapping().keys()[colIndex] attrValue = getattr(modelObj,attrName) #Check to see if there are cell formatters so that the correct value is searched for in the model if attrName in self._cellFormatters: attrValue = self._cellFormatters[attrName](attrValue) matchingIndex = self.searchModel(colIndex, attrValue) if matchingIndex.isValid(): if replace: existingId = matchingIndex.data() #Delete old record from db entityModels = self._modelInstanceFromIds([existingId]) if len(entityModels) > 0: entityModels[0].delete() self._removeRow(matchingIndex.row()) break else: #Break. Do not add item to the list. return if not self._supportsLists and self._tableModel.rowCount() > 0: self._removeRow(0) self._insertModelToView(modelObj) def _insertModelToView(self,modelObj): ''' Insert the given database model instance into the view. ''' insertPosition = self._tableModel.rowCount() self._tableModel.insertRows(insertPosition, 1) for i,attr in enumerate(self._dbModel.displayMapping().keys()): propIndex = self._tableModel.index(insertPosition,i) attrVal = getattr(modelObj,attr) #Check if there are display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) self._tableModel.setData(propIndex, attrVal) #Raise signal once entity has been inserted self.afterEntityAdded.emit(modelObj) def onAddEntity(self): ''' Slot raised on selecting to add related entities that will be mapped to the primary database model instance. ''' if self._entitySelector != None: entitySelector = self._entitySelector(self,self._entitySelectorState) #Cascade cell formatters entitySelector.setCellFormatters(self._cellFormatters) self.connect(entitySelector, SIGNAL("recordSelected(int)"),self._onRecordSelectedEntityBrowser) #self.connect(entitySelector, SIGNAL("destroyed(QObject *)"),self.onEntitySelectorDestroyed) retStatus = entitySelector.exec_() if retStatus == QDialog.Accepted: pass else: if self._notifBar != None: msg = QApplication.translate("ForeignKeyMapper","Null instance of entity selector.") self._notifBar.clear() self._notifBar.insertErrorNotification(msg)
class ForeignKeyMapper(QWidget): """ Widget for selecting database records through an entity browser or using an ExpressionBuilder for filtering records. """ #Custom signals beforeEntityAdded = pyqtSignal("PyQt_PyObject") afterEntityAdded = pyqtSignal("PyQt_PyObject") entityRemoved = pyqtSignal("PyQt_PyObject") def __init__(self, parent=None): QWidget.__init__(self,parent) self._tbFKEntity = QTableView(self) self._tbFKEntity.setEditTriggers(QAbstractItemView.NoEditTriggers) self._tbFKEntity.setAlternatingRowColors(True) self._tbFKEntity.setSelectionBehavior(QAbstractItemView.SelectRows) self._add_entity_btn = QToolButton(self) self._add_entity_btn.setToolTip(QApplication.translate("ForeignKeyMapper","Add")) self._add_entity_btn.setIcon(QIcon(":/plugins/stdm/images/icons/add.png")) self._add_entity_btn.clicked.connect(self.onAddEntity) self._edit_entity_btn = QToolButton(self) self._edit_entity_btn.setVisible(False) self._edit_entity_btn.setToolTip(QApplication.translate("ForeignKeyMapper","Edit")) self._edit_entity_btn.setIcon(QIcon(":/plugins/stdm/images/icons/edit.png")) self._filter_entity_btn = QToolButton(self) self._filter_entity_btn.setVisible(False) self._filter_entity_btn.setToolTip(QApplication.translate("ForeignKeyMapper","Select by expression")) self._filter_entity_btn.setIcon(QIcon(":/plugins/stdm/images/icons/filter.png")) self._filter_entity_btn.clicked.connect(self.onFilterEntity) self._delete_entity_btn = QToolButton(self) self._delete_entity_btn.setToolTip(QApplication.translate("ForeignKeyMapper","Remove")) self._delete_entity_btn.setIcon(QIcon(":/plugins/stdm/images/icons/remove.png")) self._delete_entity_btn.clicked.connect(self.onRemoveEntity) layout = QVBoxLayout(self) layout.setSpacing(4) layout.setMargin(5) grid_layout = QGridLayout(self) grid_layout.setHorizontalSpacing(5) grid_layout.addWidget(self._add_entity_btn, 0, 0, 1, 1) grid_layout.addWidget(self._filter_entity_btn, 0, 1, 1, 1) grid_layout.addWidget(self._edit_entity_btn, 0, 2, 1, 1) grid_layout.addWidget(self._delete_entity_btn, 0, 3, 1, 1) grid_layout.setColumnStretch(4, 5) layout.addLayout(grid_layout) layout.addWidget(self._tbFKEntity) #Instance variables self._dbModel = None self._ds_name = "" self._omitPropertyNames = [] self._entitySelector = None self._entitySelectorState = None self._supportsLists = True self._tableModel = None self._notifBar = None self._cellFormatters = {} self._deleteOnRemove = False self._uniqueValueColIndices = OrderedDict() self.global_id = None self.display_column = None self._deferred_objects = {} self._use_expression_builder = False def initialize(self): """ Configure the mapper based on the user settings. """ from stdm.data import numeric_varchar_columns #Load headers if not self._dbModel is None: headers = [] display_cols = numeric_varchar_columns(self._ds_name) #Ensure only displayable values are included for c, dc in self._dbModel.displayMapping().iteritems(): if c in display_cols: headers.append(dc) self._tableModel = BaseSTDMTableModel([],headers,self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode(QHeaderView.Interactive) self._tbFKEntity.verticalHeader().setVisible(True) ''' If expression builder is enabled then disable edit button since mapper cannot work in both selection and editing mode. ''' if self._use_expression_builder: self._filter_entity_btn.setVisible(True) self._edit_entity_btn.setVisible(False) def databaseModel(self): ''' Returns the database model that represents the foreign key entity. ''' return self._dbModel def setDatabaseModel(self,model): ''' Set the database model that represents the foreign key entity. Model has to be a callable. ''' self._dbModel = model def data_source_name(self): """ :return: Returns the name of the data source (as specified in the database). :rtype: str """ return self._ds_name def set_data_source_name(self, ds_name): """ Set the name of the data source (as specified in the database). This will be used to construct the vector layer when filtering records using the expression builder. We are using this option since we cannot extract the table/view name from the database model. :param ds_name: Name of the data source. :type ds_name: str """ self._ds_name = ds_name def setEmitPropertyNames(self,propnames): ''' Set the property names to be omitted from the display in the table list view. ''' self._omitPropertyNames = propnames def omitPropertyNames(self): ''' Returns the property names to be omitted from the display in the table list view. ''' return self._omitPropertyNames def setCellFormatters(self,formattermapping): ''' Dictionary of attribute mappings and corresponding functions for formatting the attribute value to the display value. ''' self._cellFormatters = formattermapping def addCellFormatter(self,attributeName,formatterFunc): ''' Add a new cell formatter configuration to the collection ''' self._cellFormatters[attributeName] = formatterFunc def cellFormatters(self): """ Returns a dictionary of cell formatters used by the foreign key mapper. """ return self._cellFormatters def entitySelector(self): ''' Returns the dialog for selecting the entity objects. ''' return self._entitySelector def setEntitySelector(self, selector, state=SELECT): ''' Set the dialog for selecting entity objects. Selector must be a callable. ''' if callable(selector): self._entitySelector = selector else: self._entitySelector = selector.__class__ self._entitySelectorState = state def supportList(self): ''' Returns whether the mapper supports only one item or multiple entities i.e. one-to-one and one-to-many mapping. Default is 'True'. ''' return self._supportsLists def setSupportsList(self,supportsList): ''' Sets whether the mapper supports only one item or multiple entities i.e. one-to-one (False) and one-to-many mapping (True). ''' self._supportsLists = supportsList def setNotificationBar(self,notificationBar): ''' Set the notification bar for displaying user messages. ''' self._notifBar = notificationBar def viewModel(self): ''' Return the view model used by the table view. ''' return self._tableModel def set_expression_builder(self, state): """ Set the mapper to use QGIS expression builder as the entity selector. """ self._use_expression_builder = state def expression_builder_enabled(self): """ Returns whether the mapper has been configured to use the expression builder """ return self._use_expression_builder def deleteOnRemove(self): ''' Returns the state whether a record should be deleted from the database when it is removed from the list. ''' return self._deleteOnRemove def setDeleteonRemove(self,delete): ''' Set whether whether a record should be deleted from the database when it is removed from the list. ''' self._deleteOnRemove = delete def addUniqueColumnName(self,colName,replace = True): ''' Set the name of the column whose values are to be unique. If 'replace' is True then the existing row will be replaced with one with the new value; else, the new row will not be added to the list. ''' headers = self._dbModel.displayMapping().values() colIndex = getIndex(headers,colName) if colIndex != -1: self.addUniqueColumnIndex(colIndex, replace) def addUniqueColumnIndex(self,colIndex,replace = True): ''' Set the index of the column whose values are to be unique. The column indices are zero-based. If 'replace' is True then the existing row will be replaced with the new value; else, the new row will not be added to the list. For multiple replace rules defined, then the first one added to the collection is the one that will be applied. ''' self._uniqueValueColIndices[colIndex] = replace def onFilterEntity(self): """ Slot raised to load the expression builder dialog. """ vl, msg = self.vector_layer() if vl is None: msg = msg + "\n" + QApplication.translate("ForeignKeyMapper", "The expression builder cannot be used at this moment.") QMessageBox.critical(self, QApplication.translate("ForeignKeyMapper", "Expression Builder"), msg) return if callable(self._dbModel): context = self._dbModel.__name__ else: context = self._dbModel.__class__.__name__ filter_dlg = ForeignKeyMapperExpressionDialog(vl, self, context=context) filter_dlg.setWindowTitle(QApplication.translate("ForeignKeyMapper", "Filter By Expression")) filter_dlg.recordSelected[int].connect(self._onRecordSelectedEntityBrowser) res = filter_dlg.exec_() def _removeRow(self,rowNumber): ''' Remove the row at the given index. ''' self._tableModel.removeRows(rowNumber, 1) def onRemoveEntity(self): ''' Slot raised on clicking to remove the selected entity. ''' selectedRowIndices = self._tbFKEntity.selectionModel().selectedRows(0) if len(selectedRowIndices) == 0: msg = QApplication.translate("ForeignKeyMapper","Please select the record to be removed.") self._notifBar.clear() self._notifBar.insertWarningNotification(msg) for selectedRowIndex in selectedRowIndices: #Delete record from database if flag has been set to True recId= selectedRowIndex.data() dbHandler = self._dbModel() delRec = dbHandler.queryObject().filter(self._dbModel.id == recId).first() if not delRec is None: self.entityRemoved.emit(delRec) if self._deleteOnRemove: delRec.delete() self._removeRow(selectedRowIndex.row()) def _recordIds(self): ''' Returns the primary keys of the records in the table. ''' recordIds = [] if self._tableModel: rowCount = self._tableModel.rowCount() for r in range(rowCount): #Get ID value modelIndex = self._tableModel.index(r,0) modelId = modelIndex.data() recordIds.append(modelId) return recordIds def entities(self): ''' Returns the model instance(s) depending on the configuration specified by the user. ''' recIds = self._recordIds() modelInstances = self._modelInstanceFromIds(recIds) if len(modelInstances) == 0: if self._supportsLists: return [] else: return None else: if self._supportsLists: return modelInstances else: return modelInstances[0] def setEntities(self, entities): ''' Insert entities into the table. ''' if isinstance(entities,list): for entity in entities: self._insertModelToView(entity) else: self._insertModelToView(entities) def searchModel(self, columnIndex, columnValue): ''' Searches for 'columnValue' in the column whose index is specified by 'columnIndex' in all rows contained in the model. ''' if isinstance (columnValue, QVariant): columnValue = unicode(columnValue.toString()) if not isinstance(columnValue, str) or \ not isinstance(columnValue, unicode): columnValue = unicode(columnValue) columnValue = columnValue.strip() proxy = QSortFilterProxyModel(self) proxy.setSourceModel(self._tableModel) proxy.setFilterKeyColumn(columnIndex) proxy.setFilterFixedString(columnValue) #Will return model index containing the primary key. matchingIndex = proxy.mapToSource(proxy.index(0,0)) return matchingIndex def _modelInstanceFromIds(self,ids): ''' Returns the model instance based the value of its primary key. ''' dbHandler = self._dbModel() modelInstances = [] for modelId in ids: modelObj = dbHandler.queryObject().filter(self._dbModel.id == modelId).first() if not modelObj is None: modelInstances.append(modelObj) return modelInstances def _onRecordSelectedEntityBrowser(self, rec, row_number=-1): ''' Slot raised when the user has clicked the select button in the 'EntityBrowser' dialog to add the selected record to the table's list. Add the record to the foreign key table using the mappings. ''' #Check if the record exists using the primary key so as to ensure only one instance is added to the table try: if isinstance(rec, int): recIndex = getIndex(self._recordIds(), rec) if recIndex != -1: return dbHandler = self._dbModel() modelObj = dbHandler.queryObject().filter(self._dbModel.id == rec).first() elif isinstance(rec, object): modelObj = rec else: return except: pass dbHandler = self._dbModel() modelObj = dbHandler.queryObject().filter(self._dbModel.id == rec).first() if not modelObj is None: #Raise before entity added signal self.beforeEntityAdded.emit(modelObj) #Validate for unique value configurations if not self._validate_unique_columns(modelObj, row_number): return if self._tableModel: if not self._supportsLists and self._tableModel.rowCount() > 0: self._removeRow(0) insert_position = self._insertModelToView(modelObj, row_number) if isinstance(rec, object): self._deferred_objects[insert_position] = modelObj else: try: self.global_id = self.onfk_lookup(modelObj) except Exception as ex: QMessageBox.information(self, QApplication.translate("ForeignKeyMapper", "Foreign Key Reference"), unicode(ex.message)) return def set_model_display_column(self, name): """ :return: """ self.display_column = name def onfk_lookup(self,obj, index=None): """ :param Model obj: :param : :return: """ display_label = None base_id = getattr(obj, 'id') col_list = self._dbModel.displayMapping().keys() if self.display_column: display_label = getattr(obj, self.display_column) else: display_label = getattr(obj, col_list[1]) fk_reference = FKBrowserProperty(base_id, display_label) return fk_reference def _validate_unique_columns(self,model,exclude_row = -1): """ Loop through the attributes of the model to assert for existing row values that should be unique based on the configuration of unique columns. """ for colIndex,replace in self._uniqueValueColIndices.items(): attrName = self._dbModel.displayMapping().keys()[colIndex] attrValue = getattr(model,attrName) #Check to see if there are cell formatters so that the correct value is searched for in the model if attrName in self._cellFormatters: attrValue = self._cellFormatters[attrName](attrValue) matchingIndex = self.searchModel(colIndex, attrValue) if matchingIndex.isValid() and matchingIndex.row() != exclude_row: if replace: existingId = matchingIndex.data() #Delete old record from db entityModels = self._modelInstanceFromIds([existingId]) if len(entityModels) > 0: entityModels[0].delete() self._removeRow(matchingIndex.row()) return True else: #Break. Do not add item to the list. return False return True def _insertModelToView(self, model_obj, row_number = -1): """ Insert the given database model instance into the view at the given row number position. """ if row_number == -1: row_number = self._tableModel.rowCount() self._tableModel.insertRows(row_number, 1) ''' Reset model so that we get the correct table mapping. This is a hack, will get a better solution in the future. ''' model = DeclareMapping.instance().tableMapping(self._dbModel.__name__.lower()) for i,attr in enumerate(self._dbModel.displayMapping().keys()): propIndex = self._tableModel.index(row_number,i) attrVal = getattr(model_obj, attr) #Check if there are display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) self._tableModel.setData(propIndex, attrVal) #Raise signal once entity has been inserted self.afterEntityAdded.emit(model_obj) return row_number def vector_layer(self): """ Returns a QgsVectorLayer based on the configuration information specified in the mapper including the system-wide data connection properties. """ from stdm.data import vector_layer if self._dbModel is None: msg = QApplication.translate("ForeignKeyMapper", "Primary database model object not defined.") return None, msg filter_layer = vector_layer(self._ds_name) if filter_layer is None: msg = QApplication.translate("ForeignKeyMapper", "Vector layer could not be constructed from the database table.") return None, msg if not filter_layer.isValid(): trans_msg = QApplication.translate("ForeignKeyMapper", u"The vector layer for '{0}' table is invalid.") msg = trans_msg.format(self._ds_name) return None, msg return filter_layer, "" def onAddEntity(self): """ Slot raised on selecting to add related entities that will be mapped to the primary database model instance. """ if self._tableModel: if not self._entitySelector is None: entitySelector = self._entitySelector(self, self._dbModel, self._entitySelectorState) #Cascade cell formatters entitySelector.setCellFormatters(self._cellFormatters) entitySelector.recordSelected[int].connect(self._onRecordSelectedEntityBrowser) retStatus = entitySelector.exec_() if retStatus == QDialog.Accepted: pass else: if not self._notifBar is None: msg = QApplication.translate("ForeignKeyMapper","Null instance of entity selector.") self._notifBar.clear() self._notifBar.insertErrorNotification(msg) else: entitySelector = self._entitySelector(self, self._dbModel, self._entitySelectorState) entitySelector.recordSelected[int].connect(self._onRecordSelectedEntityBrowser) retStatus = entitySelector.exec_() if retStatus == QDialog.Accepted: pass
class EntityBrowser(QDialog,Ui_EntityBrowser,SupportsManageMixin): ''' Database model entity browser. ''' ''' Custom signal that is raised when the dialog is in SELECT state. It contains the record id of the selected row. ''' recordSelected = pyqtSignal(int) def __init__(self,parent=None,dataModel = None,state = MANAGE): QDialog.__init__(self,parent) self.setupUi(self) SupportsManageMixin.__init__(self,state) self._dbmodel = dataModel self._state = state self._tableModel = None self._dataInitialized = False self._notifBar = NotificationBar(self.vlNotification) self._cellFormatters = {} #self._dateFormatter = dateFormatter #Connect signals self.connect(self.buttonBox,SIGNAL("accepted ()"),self.onAccept) self.connect(self.tbEntity,SIGNAL("doubleClicked (const QModelIndex&)"),self.onDoubleClickView) def setDatabaseModel(self,databaseModel): ''' Set the database model that represents the entity for browsing its corresponding records. ''' self._dbmodel = databaseModel def dateFormatter(self): """ Function for formatting date values """ return self._dateFormatter def setDateFormatter(self,formatter): """ Sets the function for formatting date values. Overrides the default function. """ self._dateFormatter = formatter def state(self): ''' Returns the current state that the dialog has been configured in. ''' return self._state def setState(self,state): ''' Set the state of the dialog. ''' self._state = state def title(self): ''' Set the title of the entity browser dialog. Protected method to be overriden by subclasses. ''' return "" def setCellFormatters(self,formattermapping): ''' Dictionary of attribute mappings and corresponding functions for formatting the attribute value to the display value. ''' self._cellFormatters = formattermapping def addCellFormatter(self,attributeName,formatterFunc): ''' Add a new cell formatter configuration to the collection ''' self._cellFormatters[attributeName] = formatterFunc def showEvent(self,showEvent): ''' Override event for loading the database records once the dialog is visible. This is for improved user experience i.e. to prevent the dialog from taking long to load. ''' self.setWindowTitle(self.title()) if self._dataInitialized: return if self._dbmodel != None: self._initializeData() self._dataInitialized = True def hideEvent(self,hideEvent): ''' Override event which just sets a flag to indicate that the data records have already been initialized. ''' pass def recomputeRecordCount(self): ''' Get the number of records in the specified table and updates the window title. ''' entity = self._dbmodel() #Get number of records numRecords = entity.queryObject().count() rowStr = "row" if numRecords == 1 else "rows" windowTitle = "{0} - {1} {2}".format(str(self.title()), \ str(QApplication.translate("EntityBrowser", str(numRecords))),rowStr) self.setWindowTitle(windowTitle) return numRecords def _initializeData(self): ''' Set table model and load data into it. ''' if self._dbmodel != None: headers = self._dbmodel.displayMapping().values() modelAttrs = self._dbmodel.displayMapping().keys() ''' Load entity data. There might be a better way in future in order to ensure that there is a balance between user data discovery experience and performance. ''' numRecords = self.recomputeRecordCount() #Load progress dialog progressLabel = QApplication.translate("EntityBrowser", "Fetching Records...") progressDialog = QProgressDialog(progressLabel,"",0,numRecords,self) entity = self._dbmodel() entityRecords = entity.queryObject().filter().all() #entityRecordsList = [[getattr(er,attr)for attr in modelAttrs]for er in entityRecords] #Add records to nested list for enumeration in table model entityRecordsList = [] for i,er in enumerate(entityRecords): entityRowInfo = [] progressDialog.setValue(i) try: for attr in modelAttrs: attrVal = getattr(er,attr) #Check if there are display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) if not attr in self._cellFormatters and isinstance(attrVal,date): attrVal = dateFormatter(attrVal) entityRowInfo.append(attrVal) except Exception as ex: QMessageBox.information(None, QApplication.translate("EntityBrowser", "Loading dialog"), str(ex.message) ) return entityRecordsList.append(entityRowInfo) #Set maximum value of the progress dialog progressDialog.setValue(numRecords) self._tableModel = BaseSTDMTableModel(entityRecordsList,headers,self) #Add filter columns self.cboFilterColumn.addItems(headers) #Use sortfilter proxy model for the view self._proxyModel = QSortFilterProxyModel() self._proxyModel.setDynamicSortFilter(True) self._proxyModel.setSourceModel(self._tableModel) self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self._proxyModel.setFilterKeyColumn(1) self.tbEntity.setModel(self._proxyModel) self.tbEntity.setSortingEnabled(True) self.tbEntity.sortByColumn(1,Qt.AscendingOrder) #First (ID) column will always be hidden self.tbEntity.hideColumn(0) self.cboFilterColumn.removeItem(0) self.tbEntity.horizontalHeader().setResizeMode(QHeaderView.Stretch) #Connect signals self.connect(self.cboFilterColumn, SIGNAL("currentIndexChanged (int)"),self.onFilterColumnChanged) self.connect(self.txtFilterPattern, SIGNAL("textChanged(const QString&)"),self.onFilterRegExpChanged) def onFilterColumnChanged(self,index): ''' Set the filter column for the proxy model. ''' self._proxyModel.setFilterKeyColumn((index + 1)) def onFilterRegExpChanged(self,text): ''' Slot raised whenever the filter text changes. ''' regExp =QRegExp(text,Qt.CaseInsensitive,QRegExp.FixedString) self._proxyModel.setFilterRegExp(regExp) def onDoubleClickView(self,modelindex): ''' Slot raised upon double clicking the table view. To be implemented by subclasses. ''' pass def _selectedIds(self): ''' Get the IDs of the selected row in the table view. ''' self._notifBar.clear() selectedIds = [] selRowIndices = self.tbEntity.selectionModel().selectedRows(0) if len(selRowIndices) == 0: msg = QApplication.translate("EntityBrowser", "Please select a record from the table.") self._notifBar.insertWarningNotification(msg) return selectedIds for proxyRowIndex in selRowIndices: #Get the index of the source or else the row items will have unpredictable behavior rowIndex = self._proxyModel.mapToSource(proxyRowIndex) entityId = rowIndex.data(Qt.DisplayRole) selectedIds.append(entityId) return selectedIds def onAccept(self): ''' Slot raised when user clicks to accept the dialog. The resulting action will be dependent on the state that the browser is currently configured in. ''' selIDs = self._selectedIds() if len(selIDs) == 0: return if self._mode == SELECT: #Get the first selected id selId = selIDs[0] self.recordSelected.emit(selId) self._notifBar.insertInfoNotification(QApplication.translate("EntityBrowser", "Record has been selected")) def addModelToView(self,modelObj): ''' Convenience method for adding model info into the view. ''' try: insertPosition = self._tableModel.rowCount() self._tableModel.insertRows(insertPosition,1) for i,attr in enumerate(self._dbmodel.displayMapping().keys()): propIndex = self._tableModel.index(insertPosition, i) if hasattr(modelObj, attr): attrVal = getattr(modelObj, attr) #QMessageBox.information(self, 'model',"propertyindex;{0}\nattributeVal;{1}".format(str(propIndex), str(attrVal))) #Check if there re display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) if not attr in self._cellFormatters and isinstance(attrVal, date): attrVal = dateFormatter(attrVal) self._tableModel.setData(propIndex, attrVal) except Exception as ex: QMessageBox.information(self, QApplication.translate("EntityBrowser", "Updating row"), str(ex.message)) return def _modelFromID(self,recordid): ''' Convenience method that returns the model object based on its ID. ''' dbHandler = self._dbmodel() modelObj = dbHandler.queryObject().filter(self._dbmodel.id == recordid).first() return modelObj if modelObj != None else None
def _initializeData(self): ''' Set table model and load data into it. ''' if self._dbmodel != None: headers = self._dbmodel.displayMapping().values() modelAttrs = self._dbmodel.displayMapping().keys() ''' Load entity data. There might be a better way in future in order to ensure that there is a balance between user data discovery experience and performance. ''' numRecords = self.recomputeRecordCount() #Load progress dialog progressLabel = QApplication.translate("EntityBrowser", "Fetching Records...") progressDialog = QProgressDialog(progressLabel,"",0,numRecords,self) entity = self._dbmodel() entityRecords = entity.queryObject().filter().all() #entityRecordsList = [[getattr(er,attr)for attr in modelAttrs]for er in entityRecords] #Add records to nested list for enumeration in table model entityRecordsList = [] for i,er in enumerate(entityRecords): entityRowInfo = [] progressDialog.setValue(i) try: for attr in modelAttrs: attrVal = getattr(er,attr) #Check if there are display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) if not attr in self._cellFormatters and isinstance(attrVal,date): attrVal = dateFormatter(attrVal) entityRowInfo.append(attrVal) except Exception as ex: QMessageBox.information(None, QApplication.translate("EntityBrowser", "Loading dialog"), str(ex.message) ) return entityRecordsList.append(entityRowInfo) #Set maximum value of the progress dialog progressDialog.setValue(numRecords) self._tableModel = BaseSTDMTableModel(entityRecordsList,headers,self) #Add filter columns self.cboFilterColumn.addItems(headers) #Use sortfilter proxy model for the view self._proxyModel = QSortFilterProxyModel() self._proxyModel.setDynamicSortFilter(True) self._proxyModel.setSourceModel(self._tableModel) self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self._proxyModel.setFilterKeyColumn(1) self.tbEntity.setModel(self._proxyModel) self.tbEntity.setSortingEnabled(True) self.tbEntity.sortByColumn(1,Qt.AscendingOrder) #First (ID) column will always be hidden self.tbEntity.hideColumn(0) self.cboFilterColumn.removeItem(0) self.tbEntity.horizontalHeader().setResizeMode(QHeaderView.Stretch) #Connect signals self.connect(self.cboFilterColumn, SIGNAL("currentIndexChanged (int)"),self.onFilterColumnChanged) self.connect(self.txtFilterPattern, SIGNAL("textChanged(const QString&)"),self.onFilterRegExpChanged)
class ForeignKeyMapper(QWidget): ''' Foreign key mapper widget. ''' #Custom signals beforeEntityAdded = pyqtSignal("PyQt_PyObject") afterEntityAdded = pyqtSignal("PyQt_PyObject") entityRemoved = pyqtSignal("PyQt_PyObject") def __init__(self, parent=None): QWidget.__init__(self, parent) self._tbFKEntity = QTableView(self) self._tbFKEntity.setEditTriggers(QAbstractItemView.NoEditTriggers) self._tbFKEntity.setAlternatingRowColors(True) self._tbFKEntity.setSelectionBehavior(QAbstractItemView.SelectRows) tbActions = QToolBar() tbActions.setIconSize(QSize(16, 16)) self._addEntityAction = QAction( QIcon(":/plugins/stdm/images/icons/add.png"), QApplication.translate("ForeignKeyMapper", "Add"), self) self.connect(self._addEntityAction, SIGNAL("triggered()"), self.onAddEntity) self._removeEntityAction = QAction( QIcon(":/plugins/stdm/images/icons/remove.png"), QApplication.translate("ForeignKeyMapper", "Remove"), self) self.connect(self._removeEntityAction, SIGNAL("triggered()"), self.onRemoveEntity) tbActions.addAction(self._addEntityAction) tbActions.addAction(self._removeEntityAction) layout = QVBoxLayout(self) layout.setSpacing(2) layout.setMargin(5) layout.addWidget(tbActions) layout.addWidget(self._tbFKEntity) #Instance variables self._dbModel = None self._omitPropertyNames = [] self._entitySelector = None self._entitySelectorState = None self._supportsLists = True self._tableModel = None self._notifBar = None self._cellFormatters = {} self._deleteOnRemove = False self._uniqueValueColIndices = OrderedDict() def initialize(self): ''' Configure the mapper based on the user settings. ''' #Load headers if self._dbModel != None: headers = self._dbModel.displayMapping().values() self._tableModel = BaseSTDMTableModel([], headers, self) self._tbFKEntity.setModel(self._tableModel) #First (ID) column will always be hidden self._tbFKEntity.hideColumn(0) self._tbFKEntity.horizontalHeader().setResizeMode( QHeaderView.Stretch) def databaseModel(self): ''' Returns the database model that represents the foreign key entity. ''' return self._dbModel def setDatabaseModel(self, model): ''' Set the database model that represents the foreign key entity. Model has to be a callable. ''' self._dbModel = model def setEmitPropertyNames(self, propnames): ''' Set the property names to be omitted from the display in the table list view. ''' self._omitPropertyNames = propnames def omitPropertyNames(self): ''' Returns the property names to be omitted from the display in the table list view. ''' return self._omitPropertyNames def setCellFormatters(self, formattermapping): ''' Dictionary of attribute mappings and corresponding functions for formatting the attribute value to the display value. ''' self._cellFormatters = formattermapping def addCellFormatter(self, attributeName, formatterFunc): ''' Add a new cell formatter configuration to the collection ''' self._cellFormatters[attributeName] = formatterFunc def cellFormatters(self): """ Returns a dictionary of cell formatters used by the foreign key mapper. """ return self._cellFormatters def entitySelector(self): ''' Returns the dialog for selecting the entity objects. ''' return self._entitySelector def setEntitySelector(self, selector, state=SELECT): ''' Set the dialog for selecting entity objects. Selector must be a callable. ''' self._entitySelector = selector self._entitySelectorState = state def supportList(self): ''' Returns whether the mapper supports only one item or multiple entities i.e. one-to-one and one-to-many mapping. Default is 'True'. ''' return self._supportsLists def setSupportsList(self, supportsList): ''' Sets whether the mapper supports only one item or multiple entities i.e. one-to-one (False) and one-to-many mapping (True). ''' self._supportsLists = supportsList def setNotificationBar(self, notificationBar): ''' Set the notification bar for displaying user messages. ''' self._notifBar = notificationBar def viewModel(self): ''' Return the view model used by the table view. ''' return self._tableModel def deleteOnRemove(self): ''' Returns the state whether a record should be deleted from the database when it is removed from the list. ''' return self._deleteOnRemove def setDeleteonRemove(self, delete): ''' Set whether whether a record should be deleted from the database when it is removed from the list. ''' self._deleteOnRemove = delete def addUniqueColumnName(self, colName, replace=True): ''' Set the name of the column whose values are to be unique. If 'replace' is True then the existing row will be replaced with one with the new value; else, the new row will not be added to the list. ''' headers = self._dbModel.displayMapping().values() colIndex = getIndex(headers, colName) if colIndex != -1: self.addUniqueColumnIndex(colIndex, replace) def addUniqueColumnIndex(self, colIndex, replace=True): ''' Set the index of the column whose values are to be unique. The column indices are zero-based. If 'replace' is True then the existing row will be replaced with the new value; else, the new row will not be added to the list. For multiple replace rules defined, then the first one added to the collection is the one that will be applied. ''' self._uniqueValueColIndices[colIndex] = replace def _removeRow(self, rowNumber): ''' Remove the row at the given index. ''' self._tableModel.removeRows(rowNumber, 1) def onRemoveEntity(self): ''' Slot raised on clicking to remove the selected entity. ''' selectedRowIndices = self._tbFKEntity.selectionModel().selectedRows(0) if len(selectedRowIndices) == 0: msg = QApplication.translate( "ForeignKeyMapper", "Please select the record to be removed.") self._notifBar.clear() self._notifBar.insertWarningNotification(msg) for selectedRowIndex in selectedRowIndices: #Delete record from database if flag has been set to True recId = selectedRowIndex.data() dbHandler = self._dbModel() delRec = dbHandler.queryObject().filter( self._dbModel.id == recId).first() if not delRec is None: self.entityRemoved.emit(delRec) if self._deleteOnRemove: delRec.delete() self._removeRow(selectedRowIndex.row()) def _recordIds(self): ''' Returns the primary keys of the records in the table. ''' rowCount = self._tableModel.rowCount() recordIds = [] for r in range(rowCount): #Get ID value modelIndex = self._tableModel.index(r, 0) modelId = modelIndex.data() recordIds.append(modelId) return recordIds def entities(self): ''' Returns the model instance(s) depending on the configuration specified by the user. ''' recIds = self._recordIds() modelInstances = self._modelInstanceFromIds(recIds) if len(modelInstances) == 0: return None else: if self._supportsLists: return modelInstances else: return modelInstances[0] def setEntities(self, entities): ''' Insert entities into the table. ''' if isinstance(entities, list): for entity in entities: self._insertModelToView(entity) else: self._insertModelToView(entities) def searchModel(self, columnIndex, columnValue): ''' Searches for 'columnValue' in the column whose index is specified by 'columnIndex' in all rows contained in the model. ''' if isinstance(columnValue, QVariant): columnValue = str(columnValue.toString()) if not isinstance(columnValue, str): columnValue = str(columnValue) columnValue = columnValue.strip() proxy = QSortFilterProxyModel(self) proxy.setSourceModel(self._tableModel) proxy.setFilterKeyColumn(columnIndex) proxy.setFilterFixedString(columnValue) #Will return model index containing the primary key. matchingIndex = proxy.mapToSource(proxy.index(0, 0)) return matchingIndex def _modelInstanceFromIds(self, ids): ''' Returns the model instance based the value of its primary key. ''' dbHandler = self._dbModel() modelInstances = [] for modelId in ids: modelObj = dbHandler.queryObject().filter( self._dbModel.id == modelId).first() if modelObj != None: modelInstances.append(modelObj) return modelInstances def _onRecordSelectedEntityBrowser(self, recid): ''' Slot raised when the user has clicked the select button in the 'EntityBrowser' dialog to add the selected record to the table's list. Add the record to the foreign key table using the mappings. ''' #Check if the record exists using the primary key so as to ensure only one instance is added to the table recIndex = getIndex(self._recordIds(), id) if recIndex != -1: return dbHandler = self._dbModel() modelObj = dbHandler.queryObject().filter( self._dbModel.id == recid).first() if modelObj != None: #Raise before entity added signal self.beforeEntityAdded.emit(modelObj) #Validate for unique value configurations for colIndex, replace in self._uniqueValueColIndices.items(): attrName = self._dbModel.displayMapping().keys()[colIndex] attrValue = getattr(modelObj, attrName) #Check to see if there are cell formatters so that the correct value is searched for in the model if attrName in self._cellFormatters: attrValue = self._cellFormatters[attrName](attrValue) matchingIndex = self.searchModel(colIndex, attrValue) if matchingIndex.isValid(): if replace: existingId = matchingIndex.data() #Delete old record from db entityModels = self._modelInstanceFromIds([existingId]) if len(entityModels) > 0: entityModels[0].delete() self._removeRow(matchingIndex.row()) break else: #Break. Do not add item to the list. return if not self._supportsLists and self._tableModel.rowCount() > 0: self._removeRow(0) self._insertModelToView(modelObj) def _insertModelToView(self, modelObj): ''' Insert the given database model instance into the view. ''' insertPosition = self._tableModel.rowCount() self._tableModel.insertRows(insertPosition, 1) for i, attr in enumerate(self._dbModel.displayMapping().keys()): propIndex = self._tableModel.index(insertPosition, i) attrVal = getattr(modelObj, attr) #Check if there are display formatters and apply if one exists for the given attribute if attr in self._cellFormatters: attrVal = self._cellFormatters[attr](attrVal) self._tableModel.setData(propIndex, attrVal) #Raise signal once entity has been inserted self.afterEntityAdded.emit(modelObj) def onAddEntity(self): ''' Slot raised on selecting to add related entities that will be mapped to the primary database model instance. ''' if self._entitySelector != None: entitySelector = self._entitySelector(self, self._entitySelectorState) #Cascade cell formatters entitySelector.setCellFormatters(self._cellFormatters) self.connect(entitySelector, SIGNAL("recordSelected(int)"), self._onRecordSelectedEntityBrowser) #self.connect(entitySelector, SIGNAL("destroyed(QObject *)"),self.onEntitySelectorDestroyed) retStatus = entitySelector.exec_() if retStatus == QDialog.Accepted: pass else: if self._notifBar != None: msg = QApplication.translate( "ForeignKeyMapper", "Null instance of entity selector.") self._notifBar.clear() self._notifBar.insertErrorNotification(msg)