class FilteredComboBox(QComboBox): """Custom QComboBox with filtering option.""" def __init__(self, parent=None): super(FilteredComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.setEditable(True) self.filter_proxy_model = QSortFilterProxyModel(self) self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.filter_proxy_model.setSortCaseSensitivity(Qt.CaseInsensitive) self.filter_proxy_model.setSourceModel(self.model()) self.completer = QCompleter(self.filter_proxy_model, self) self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.setCompleter(self.completer) self.setMinimumSize(150, 25) self.setFont(QFont("Segoe UI", 10)) self.setStyleSheet("QComboBox {background-color: white;}") self.setMaxVisibleItems(10) self.completer.activated.connect(self.on_completer_activated) self.lineEdit().textEdited.connect( self.filter_proxy_model.setFilterFixedString) def on_completer_activated(self, text): """Set active combobox item when a completer item is picked.""" if not text: return idx = self.findText(text) self.setCurrentIndex(idx) self.activated[str].emit(self.itemText(idx)) def setModel(self, model): """Set completer model after the combobox model.""" super(FilteredComboBox, self).setModel(model) self.filter_proxy_model.setSourceModel(model) self.completer.setModel(self.filter_proxy_model) def setModelColumn(self, column_idx): """Set the correct column for completer and combobox model using column index.""" self.completer.setCompletionColumn(column_idx) self.filter_proxy_model.setFilterKeyColumn(column_idx) super(FilteredComboBox, self).setModelColumn(column_idx)
class STRViewEntityWidget(WIDGET2, BASE2, EntitySearchItem): """ A widget that represents options for searching through an entity. """ asyncStarted = pyqtSignal() asyncFinished = pyqtSignal() def __init__(self, config, formatter=None, parent=None): QWidget.__init__(self, parent) EntitySearchItem.__init__(self, formatter) self.setupUi(self) self.tbSTRViewEntity.setTabIcon(0, GuiUtils.get_icon('filter.png')) self.tbSTRViewEntity.setTabIcon(1, GuiUtils.get_icon('period_blue.png')) self.config = config self.setConfigOptions() self.curr_profile = current_profile() self.social_tenure = self.curr_profile.social_tenure self.str_model = entity_model(self.social_tenure) # Model for storing display and actual mapping values self._completer_model = None self._proxy_completer_model = None # Hook up signals self.cboFilterCol.currentIndexChanged.connect( self._on_column_index_changed) self.init_validity_dates() self.validity_from_date.dateChanged.connect(self.set_minimum_to_date) self.validity.setDisabled(True) self.init_validity_checkbox() def init_validity_checkbox(self): self.check_box_list = [] self.validity_checkbox = QCheckBox() self.check_box_list.append(self.validity_checkbox) self.tbSTRViewEntity.tabBar().setTabButton( self.tbSTRViewEntity.tabBar().count() - 1, QTabBar.LeftSide, self.validity_checkbox) self.validity_checkbox.stateChanged.connect( self.toggle_validity_period) def toggle_validity_period(self, state): if state == Qt.Checked: self.validity.setDisabled(False) else: self.validity.setDisabled(True) def set_minimum_to_date(self): """ Set the minimum to date based on the change in value of from date. :return: :rtype: """ self.validity_to_date.setMinimumDate(self.validity_from_date.date()) def init_validity_dates(self): """ Initialize the dates by setting the current date. :return: :rtype: """ self.validity_from_date.setDate(date.today()) self.validity_to_date.setDate(date.today()) def setConfigOptions(self): """ Apply configuration options. """ # Set filter columns and remove id column for col_name, display_name in self.config.filterColumns.items(): if col_name != "id": self.cboFilterCol.addItem(display_name, col_name) def loadAsync(self): """ Asynchronously loads an entity's attribute values. """ self.asyncStarted.emit() # Create model worker workerThread = QThread(self) modelWorker = ModelWorker() modelWorker.moveToThread(workerThread) # Connect signals modelWorker.error.connect(self.errorHandler) workerThread.started.connect(lambda: modelWorker.fetch( self.config.STRModel, self.currentFieldName())) modelWorker.retrieved.connect(self._asyncFinished) modelWorker.retrieved.connect(workerThread.quit) workerThread.finished.connect(modelWorker.deleteLater) workerThread.finished.connect(workerThread.deleteLater) # Start thread workerThread.start() def validate(self): """ Validate entity search widget """ is_valid = True message = "" if self.txtFilterPattern.text() == "": message = QApplication.translate("ViewSTR", "Search word cannot be empty.") is_valid = False return is_valid, message def executeSearch(self): """ Base class override. Search for matching items for the specified entity and column. """ model_root_node = None prog_dialog = QProgressDialog(self) prog_dialog.setFixedWidth(380) prog_dialog.setWindowTitle( QApplication.translate("STRViewEntityWidget", "Searching for STR...")) prog_dialog.show() prog_dialog.setRange(0, 10) search_term = self._searchTerm() prog_dialog.setValue(2) # Try to get the corresponding search term value from the completer model if not self._completer_model is None: reg_exp = QRegExp("^%s$" % (search_term), Qt.CaseInsensitive, QRegExp.RegExp2) self._proxy_completer_model.setFilterRegExp(reg_exp) if self._proxy_completer_model.rowCount() > 0: # Get corresponding actual value from the first matching item value_model_idx = self._proxy_completer_model.index(0, 1) source_model_idx = self._proxy_completer_model.mapToSource( value_model_idx) prog_dialog.setValue(4) search_term = self._completer_model.data( source_model_idx, Qt.DisplayRole) modelInstance = self.config.STRModel() modelQueryObj = modelInstance.queryObject() queryObjProperty = getattr(self.config.STRModel, self.currentFieldName()) entity_name = modelQueryObj._primary_entity._label_name entity = self.curr_profile.entity_by_name(entity_name) prog_dialog.setValue(6) # Get property type so that the filter can # be applied according to the appropriate type propType = queryObjProperty.property.columns[0].type results = [] try: if not isinstance(propType, String): col_name = self.currentFieldName() col = entity.columns[self.currentFieldName()] if col.TYPE_INFO == 'LOOKUP': lookup_entity = lookup_parent_entity( self.curr_profile, col_name) lkp_model = entity_model(lookup_entity) lkp_obj = lkp_model() value_obj = getattr(lkp_model, 'value') result = lkp_obj.queryObject().filter( func.lower(value_obj) == func.lower( search_term)).first() if result is None: result = lkp_obj.queryObject().filter( func.lower(value_obj).like(search_term + '%')).first() if not result is None: results = modelQueryObj.filter( queryObjProperty == result.id).all() else: results = [] else: results = modelQueryObj.filter( func.lower(queryObjProperty) == func.lower( search_term)).all() if self.validity.isEnabled(): valid_str_ids = self.str_validity_period_filter(results) else: valid_str_ids = None prog_dialog.setValue(7) except exc.StatementError: return model_root_node, [], search_term # if self.formatter is not None: # self.formatter.setData(results) # model_root_node = self.formatter.root(valid_str_ids) prog_dialog.setValue(10) prog_dialog.hide() return results, search_term def str_validity_period_filter(self, results): """ Filter the entity results using validity period in STR table. :param results: Entity result :type results: SQLAlchemy result proxy :return: Valid list of STR ids :rtype: List """ self.str_model_obj = self.str_model() valid_str_ids = [] for result in results: from_date = self.validity_from_date.date().toPyDate() to_date = self.validity_to_date.date().toPyDate() entity_id = '{}_id'.format(result.__table__.name[3:]) str_column_obj = getattr(self.str_model, entity_id) str_result = self.str_model_obj.queryObject().filter( self.str_model.validity_start >= from_date).filter( self.str_model.validity_end <= to_date).filter( str_column_obj == result.id).all() for res in str_result: valid_str_ids.append(res.id) return valid_str_ids def reset(self): """ Clear search input parameters. """ self.txtFilterPattern.clear() if self.cboFilterCol.count() > 0: self.cboFilterCol.setCurrentIndex(0) def currentFieldName(self): """ Returns the name of the database field from the current item in the combo box. """ curr_index = self.cboFilterCol.currentIndex() field_name = self.cboFilterCol.itemData(curr_index) if field_name is None: return else: return field_name def _searchTerm(self): """ Returns the search term specified by the user. """ return self.txtFilterPattern.text() def _asyncFinished(self, model_values): """ Slot raised when worker has finished retrieving items. """ # Create QCompleter and add values to it. self._update_completer(model_values) self.asyncFinished.emit() def _update_completer(self, values): # Get the items in a tuple and put them in a list # Store display and actual values in a # model for easier mapping and # retrieval when carrying out searches model_attr_mapping = [] # Check if there are formaters specified # for the current field name for mv in values: f_model_values = [] m_val = mv[0] if m_val is not None: col_label = self.currentFieldName() if col_label in self.config.LookupFormatters: formatter = self.config.LookupFormatters[col_label] if formatter.column.TYPE_INFO == 'LOOKUP': m_val = formatter.code_value(m_val)[0] else: m_val = formatter.format_column_value(m_val) f_model_values.extend([m_val, m_val]) model_attr_mapping.append(f_model_values) self._completer_model = BaseSTDMTableModel(model_attr_mapping, ["", ""], self) # We will use the QSortFilterProxyModel for filtering purposes self._proxy_completer_model = QSortFilterProxyModel() self._proxy_completer_model.setDynamicSortFilter(True) self._proxy_completer_model.setSourceModel(self._completer_model) self._proxy_completer_model.setSortCaseSensitivity(Qt.CaseInsensitive) self._proxy_completer_model.setFilterKeyColumn(0) # Configure completer mod_completer = QCompleter(self._completer_model, self) mod_completer.setCaseSensitivity(Qt.CaseInsensitive) mod_completer.setCompletionMode(QCompleter.PopupCompletion) mod_completer.setCompletionColumn(0) mod_completer.setCompletionRole(Qt.DisplayRole) self.txtFilterPattern.setCompleter(mod_completer) def _on_column_index_changed(self, int): """ Slot raised when the user selects a different filter column. """ self.txtFilterPattern.clear() self.loadAsync()
def makeProxyModel(self, model): proxyModel = QSortFilterProxyModel() proxyModel.setDynamicSortFilter(True) proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) proxyModel.setSourceModel(model) return proxyModel