Exemple #1
0
    def _initializeData(self, filtered_records=None):
        '''
        Set table model and load data into it.
        '''
        if self._dbmodel is None:
            msg = QApplication.translate(
                'EntityBrowser',
                'The data model for the entity could not be loaded, \n'
                'please contact your database administrator.')
            QMessageBox.critical(
                self, QApplication.translate('EntityBrowser',
                                             'Entity Browser'), msg)

        else:
            self._init_entity_columns()
            # 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.
            if filtered_records is not None:
                self.current_records = filtered_records.rowcount

            numRecords = self.recomputeRecordCount(init_data=True)

            # Load progress dialog
            progressLabel = QApplication.translate("EntityBrowser",
                                                   "Fetching Records...")
            progressDialog = QProgressDialog(progressLabel, None, 0,
                                             numRecords, self)

            QApplication.processEvents()
            progressDialog.show()
            progressDialog.setValue(0)

            #Add records to nested list for enumeration in table model
            load_data = True
            if self.plugin is not None:
                if self._entity.name in self.plugin.entity_table_model.keys():
                    if filtered_records is None:
                        self._tableModel = self.plugin.entity_table_model[
                            self._entity.name]
                        #load_data = False
                    #else:
                    #load_data = True

            if isinstance(self._parent, EntityEditorDialog):
                load_data = True

            if load_data:
                # Only one filter is possible.
                if filtered_records is not None:
                    entity_records = filtered_records
                else:
                    entity_records = fetch_from_table(self._entity.name,
                                                      limit=self.record_limit)

            # if self._tableModel is None:
                entity_records_collection = []
                for i, er in enumerate(entity_records):
                    if i == self.record_limit:
                        break
                    QApplication.processEvents()
                    entity_row_info = []
                    progressDialog.setValue(i)
                    try:
                        # for attr, attr_val in er.items():
                        # print e
                        for attr in self._entity_attrs:
                            # attr_val = getattr(er, attr)
                            attr_val = er[attr]

                            # Check if there are display formatters and apply if
                            # one exists for the given attribute.
                            if attr_val is not None:  # No need of formatter for None value
                                if attr in self._cell_formatters:
                                    formatter = self._cell_formatters[attr]
                                    attr_val = formatter.format_column_value(
                                        attr_val)
                            entity_row_info.append(attr_val)
                    except Exception as ex:
                        QMessageBox.critical(
                            self,
                            QApplication.translate('EntityBrowser',
                                                   'Loading Records'),
                            unicode(ex.message))
                        return

                    entity_records_collection.append(entity_row_info)

                self._tableModel = BaseSTDMTableModel(
                    entity_records_collection, self._headers, self)
                if self.plugin is not None:
                    self.plugin.entity_table_model[self._entity.name] = \
                            self._tableModel
            # Add filter columns
            for header, info in self._searchable_columns.iteritems():
                column_name, index = info['name'], info['header_index']
                if column_name != 'id':
                    self.cboFilterColumn.addItem(header, info)

            #Use sortfilter proxy model for the view
            self._proxyModel = VerticalHeaderSortFilterProxyModel()
            self._proxyModel.setDynamicSortFilter(True)
            self._proxyModel.setSourceModel(self._tableModel)
            self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)

            #USe first column in the combo for filtering
            if self.cboFilterColumn.count() > 0:
                self.set_proxy_model_filter_column(0)

            self.tbEntity.setModel(self._proxyModel)
            if numRecords < self.record_limit:
                self.tbEntity.setSortingEnabled(True)
                self.tbEntity.sortByColumn(1, Qt.AscendingOrder)

            #First (ID) column will always be hidden
            self.tbEntity.hideColumn(0)

            self.tbEntity.horizontalHeader().setResizeMode(
                QHeaderView.Interactive)

            self.tbEntity.resizeColumnsToContents()

            #Connect signals
            self.connect(self.cboFilterColumn,
                         SIGNAL('currentIndexChanged (int)'),
                         self.onFilterColumnChanged)
            self.connect(self.txtFilterPattern,
                         SIGNAL('textChanged(const QString&)'),
                         self.onFilterRegExpChanged)

            #Select record with the given ID if specified
            if not self._select_item is None:
                self._select_record(self._select_item)

            if numRecords > 0:
                # Set maximum value of the progress dialog
                progressDialog.setValue(numRecords)
            else:
                progressDialog.hide()
Exemple #2
0
class EntityBrowser(SupportsManageMixin, QDialog, Ui_EntityBrowser):
    """
    Dialog for browsing entity records in a table view.
    """

    # 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,
                 entity,
                 parent=None,
                 state=MANAGE,
                 load_records=True,
                 plugin=None):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        # Add maximize buttons
        self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint
                            | Qt.WindowMaximizeButtonHint)

        SupportsManageMixin.__init__(self, state)
        # Init document viewer setup
        self._view_docs_act = None
        viewer_title = QApplication.translate('EntityBrowser',
                                              'Document Viewer')
        self.doc_viewer_title = u'{0} {1}'.format(entity.ui_display(),
                                                  viewer_title)
        self._doc_viewer = _EntityDocumentViewerHandler(
            self.doc_viewer_title, self)
        self.load_records = load_records
        #Initialize toolbar
        self.plugin = plugin
        self.tbActions = QToolBar()
        self.tbActions.setObjectName('eb_actions_toolbar')
        self.tbActions.setIconSize(QSize(16, 16))
        self.tbActions.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.vlActions.addWidget(self.tbActions)

        self._entity = entity
        self._dbmodel = entity_model(entity)
        self._state = state
        self._tableModel = None
        self._parent = parent
        self._data_initialized = False
        self._notifBar = NotificationBar(self.vlNotification)
        self._headers = []
        self._entity_attrs = []
        self._cell_formatters = {}
        self.filtered_records = []
        self._searchable_columns = OrderedDict()
        self._show_docs_col = False
        self.child_model = OrderedDict()
        #ID of a record to select once records have been added to the table
        self._select_item = None
        self.current_records = 0

        self.record_limit = get_entity_browser_record_limit()

        #Enable viewing of supporting documents
        if self.can_view_supporting_documents:
            self._add_view_supporting_docs_btn()

        self._add_advanced_search_btn()
        #Connect signals
        self.buttonBox.accepted.connect(self.onAccept)
        self.tbEntity.doubleClicked[QModelIndex].connect(
            self.onDoubleClickView)

    def children_entities(self):
        """
        :return: Returns a list of children entities
        that refer to the main entity as the parent.
        :rtype: list
        """
        return [
            ch for ch in self._entity.children()
            if ch.TYPE_INFO == Entity.TYPE_INFO
        ]

    @property
    def entity(self):
        """
        :return: Returns the Entity object used in this browser.
        :rtype: Entity
        """
        return self._entity

    @property
    def can_view_supporting_documents(self):
        """
        :return: True if the browser supports the viewing of supporting
        documents.
        :rtype: bool
        """
        test_ent_obj = self._dbmodel()

        if self._entity.supports_documents \
                and hasattr(test_ent_obj, 'documents'):
            return True

        return False

    def _add_view_supporting_docs_btn(self):
        #Add button for viewing supporting documents if supported
        view_docs_str = QApplication.translate('EntityBrowser',
                                               'View Documents')
        self._view_docs_act = QAction(
            QIcon(':/plugins/stdm/images/icons/document.png'), view_docs_str,
            self)

        #Connect signal for showing document viewer
        self._view_docs_act.triggered.connect(self.on_load_document_viewer)

        self.tbActions.addAction(self._view_docs_act)

    def _add_advanced_search_btn(self):
        #Add button for viewing supporting documents if supported
        search_str = QApplication.translate('EntityBrowser', 'Advanced Search')
        self._search_act = QAction(
            QIcon(':/plugins/stdm/images/icons/advanced_search.png'),
            search_str, self)

        #Connect signal for showing document viewer
        self._search_act.triggered.connect(self.on_advanced_search)

        self.tbActions.addAction(self._search_act)

    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 set_selection_record_id(self, id):
        """
        Set the ID of a record to be selected only once all records have been
        added to the table view.
        :param id: Record id to be selected.
        :type id: int
        """
        self._select_item = id

    def title(self):
        '''
        Set the title of the entity browser dialog.
        Protected method to be overridden by subclasses.
        '''
        records = QApplication.translate('EntityBrowser', 'Records')
        if self._entity.label != '':
            title = self._entity.label
        else:
            title = self._entity.ui_display()

        return u'{} {}'.format(title, records)

    def setCellFormatters(self, formattermapping):
        '''
        Dictionary of attribute mappings and corresponding functions for
        formatting the attribute value to the display value.
        '''
        self._cell_formatters = formattermapping

    def addCellFormatter(self, attributeName, formatterFunc):
        '''
        Add a new cell formatter configuration to the collection
        '''
        self._cell_formatters[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(unicode(self.title()))

        if self._data_initialized:
            return
        try:
            if not self._dbmodel is None:
                # cProfile.runctx('self._initializeData()', globals(), locals())
                self._initializeData()

        except Exception as ex:
            pass

        self._data_initialized = True

    def hideEvent(self, hideEvent):
        '''
        Override event which just sets a flag to indicate that the data records have already been
        initialized.
        '''
        pass

    def clear_selection(self):
        """
        Deselects all selected items in the table view.
        """
        self.tbEntity.clearSelection()

    def clear_notifications(self):
        """
        Clears all notifications messages in the dialog.
        """
        self._notifBar.clear()

    def recomputeRecordCount(self, init_data=False):
        '''
        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()
        if init_data:
            if self.current_records < 1:
                if numRecords > self.record_limit:
                    self.current_records = self.record_limit
                else:
                    self.current_records = numRecords

        rowStr = QApplication.translate('EntityBrowser', 'row') \
            if numRecords == 1 \
            else QApplication.translate('EntityBrowser', 'rows')
        showing = QApplication.translate('EntityBrowser', 'Showing')
        windowTitle = u"{0} - {1} {2} of {3} {4}".format(
            self.title(), showing, self.current_records, numRecords, rowStr)

        self.setWindowTitle(windowTitle)

        return numRecords

    def _init_entity_columns(self):
        """
        Asserts if the entity columns actually do exist in the database. The
        method also initializes the table headers, entity column and cell
        formatters.
        """
        self._headers[:] = []
        table_name = self._entity.name
        columns = table_column_names(table_name)
        missing_columns = []

        header_idx = 0

        #Iterate entity column and assert if they exist

        for c in self._entity.columns.values():

            # Exclude geometry columns
            if isinstance(c, GeometryColumn):
                continue

            #Do not include virtual columns in list of missing columns
            if not c.name in columns and not isinstance(c, VirtualColumn):
                missing_columns.append(c.name)

            else:
                header = c.ui_display()
                self._headers.append(header)

                col_name = c.name
                '''
                If it is a virtual column then use column name as the header
                but fully qualified column name (created by SQLAlchemy
                relationship) as the entity attribute name.
                '''

                if isinstance(c, MultipleSelectColumn):
                    col_name = c.model_attribute_name

                self._entity_attrs.append(col_name)

                # Get widget factory so that we can use the value formatter
                w_factory = ColumnWidgetRegistry.factory(c.TYPE_INFO)
                if not w_factory is None:
                    formatter = w_factory(c)
                    self._cell_formatters[col_name] = formatter

                #Set searchable columns
                if c.searchable:
                    self._searchable_columns[c.ui_display()] = {
                        'name': c.name,
                        'header_index': header_idx
                    }

                header_idx += 1

        if len(missing_columns) > 0:
            msg = QApplication.translate(
                'EntityBrowser',
                u'The following columns have been defined in the '
                u'configuration but are missing in corresponding '
                u'database table, please re-run the configuration wizard '
                u'to create them.\n{0}'.format('\n'.join(missing_columns)))

            QMessageBox.warning(
                self, QApplication.translate('EntityBrowser',
                                             'Entity Browser'), msg)

    def _select_record(self, id):
        #Selects record with the given ID.
        if id is None:
            return

        m = self.tbEntity.model()
        s = self.tbEntity.selectionModel()

        start_idx = m.index(0, 0)
        idxs = m.match(start_idx, Qt.DisplayRole, id, 1, Qt.MatchExactly)

        if len(idxs) > 0:
            sel_idx = idxs[0]
            #Select item
            s.select(
                sel_idx,
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)

    def on_advanced_search(self):
        search = AdvancedSearch(self._entity, parent=self)
        search.show()

    def on_load_document_viewer(self):
        #Slot raised to show the document viewer for the selected entity
        sel_rec_ids = self._selected_record_ids()

        if len(sel_rec_ids) == 0:
            return

        #Get document objects
        ent_obj = self._dbmodel()

        for sel_id in sel_rec_ids:
            er = ent_obj.queryObject().filter(
                self._dbmodel.id == sel_id).first()
            if not er is None:
                docs = er.documents

                #Notify there are no documents for the selected doc
                if len(docs) == 0:
                    msg = QApplication.translate(
                        'EntityBrowser',
                        'There are no supporting documents for the selected record.'
                    )

                    QMessageBox.warning(self, self.doc_viewer_title, msg)

                    continue

                self._doc_viewer.load(docs)

    def _initializeData(self, filtered_records=None):
        '''
        Set table model and load data into it.
        '''
        if self._dbmodel is None:
            msg = QApplication.translate(
                'EntityBrowser',
                'The data model for the entity could not be loaded, \n'
                'please contact your database administrator.')
            QMessageBox.critical(
                self, QApplication.translate('EntityBrowser',
                                             'Entity Browser'), msg)

        else:
            self._init_entity_columns()
            # 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.
            if filtered_records is not None:
                self.current_records = filtered_records.rowcount

            numRecords = self.recomputeRecordCount(init_data=True)

            # Load progress dialog
            progressLabel = QApplication.translate("EntityBrowser",
                                                   "Fetching Records...")
            progressDialog = QProgressDialog(progressLabel, None, 0,
                                             numRecords, self)

            QApplication.processEvents()
            progressDialog.show()
            progressDialog.setValue(0)

            #Add records to nested list for enumeration in table model
            load_data = True
            if self.plugin is not None:
                if self._entity.name in self.plugin.entity_table_model.keys():
                    if filtered_records is None:
                        self._tableModel = self.plugin.entity_table_model[
                            self._entity.name]
                        #load_data = False
                    #else:
                    #load_data = True

            if isinstance(self._parent, EntityEditorDialog):
                load_data = True

            if load_data:
                # Only one filter is possible.
                if filtered_records is not None:
                    entity_records = filtered_records
                else:
                    entity_records = fetch_from_table(self._entity.name,
                                                      limit=self.record_limit)

            # if self._tableModel is None:
                entity_records_collection = []
                for i, er in enumerate(entity_records):
                    if i == self.record_limit:
                        break
                    QApplication.processEvents()
                    entity_row_info = []
                    progressDialog.setValue(i)
                    try:
                        # for attr, attr_val in er.items():
                        # print e
                        for attr in self._entity_attrs:
                            # attr_val = getattr(er, attr)
                            attr_val = er[attr]

                            # Check if there are display formatters and apply if
                            # one exists for the given attribute.
                            if attr_val is not None:  # No need of formatter for None value
                                if attr in self._cell_formatters:
                                    formatter = self._cell_formatters[attr]
                                    attr_val = formatter.format_column_value(
                                        attr_val)
                            entity_row_info.append(attr_val)
                    except Exception as ex:
                        QMessageBox.critical(
                            self,
                            QApplication.translate('EntityBrowser',
                                                   'Loading Records'),
                            unicode(ex.message))
                        return

                    entity_records_collection.append(entity_row_info)

                self._tableModel = BaseSTDMTableModel(
                    entity_records_collection, self._headers, self)
                if self.plugin is not None:
                    self.plugin.entity_table_model[self._entity.name] = \
                            self._tableModel
            # Add filter columns
            for header, info in self._searchable_columns.iteritems():
                column_name, index = info['name'], info['header_index']
                if column_name != 'id':
                    self.cboFilterColumn.addItem(header, info)

            #Use sortfilter proxy model for the view
            self._proxyModel = VerticalHeaderSortFilterProxyModel()
            self._proxyModel.setDynamicSortFilter(True)
            self._proxyModel.setSourceModel(self._tableModel)
            self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)

            #USe first column in the combo for filtering
            if self.cboFilterColumn.count() > 0:
                self.set_proxy_model_filter_column(0)

            self.tbEntity.setModel(self._proxyModel)
            if numRecords < self.record_limit:
                self.tbEntity.setSortingEnabled(True)
                self.tbEntity.sortByColumn(1, Qt.AscendingOrder)

            #First (ID) column will always be hidden
            self.tbEntity.hideColumn(0)

            self.tbEntity.horizontalHeader().setResizeMode(
                QHeaderView.Interactive)

            self.tbEntity.resizeColumnsToContents()

            #Connect signals
            self.connect(self.cboFilterColumn,
                         SIGNAL('currentIndexChanged (int)'),
                         self.onFilterColumnChanged)
            self.connect(self.txtFilterPattern,
                         SIGNAL('textChanged(const QString&)'),
                         self.onFilterRegExpChanged)

            #Select record with the given ID if specified
            if not self._select_item is None:
                self._select_record(self._select_item)

            if numRecords > 0:
                # Set maximum value of the progress dialog
                progressDialog.setValue(numRecords)
            else:
                progressDialog.hide()

    def _header_index_from_filter_combo_index(self, idx):
        col_info = self.cboFilterColumn.itemData(idx)

        return col_info['name'], col_info['header_index']

    def set_proxy_model_filter_column(self, index):
        #Set the filter column for the proxy model using the combo index
        name, header_idx = self._header_index_from_filter_combo_index(index)
        self._proxyModel.setFilterKeyColumn(header_idx)

    def onFilterColumnChanged(self, index):
        '''
        Set the filter column for the proxy model.
        '''
        self.set_proxy_model_filter_column(index)

    def _onFilterRegExpChanged(self, text):
        cProfile.runctx('self._onFilterRegExpChanged(text)', globals(),
                        locals())

    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 _selected_record_ids(self):
        '''
        Get the IDs of the selected row in the table view.
        '''
        self._notifBar.clear()

        selected_ids = []
        sel_row_indices = self.tbEntity.selectionModel().selectedRows(0)

        if len(sel_row_indices) == 0:
            msg = QApplication.translate(
                "EntityBrowser", "Please select a record from the table.")

            self._notifBar.insertWarningNotification(msg)

            return selected_ids

        for proxyRowIndex in sel_row_indices:
            #Get the index of the source or else the row items will have unpredictable behavior
            row_index = self._proxyModel.mapToSource(proxyRowIndex)
            entity_id = row_index.data(Qt.DisplayRole)
            selected_ids.append(entity_id)

        return selected_ids

    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._selected_record_ids()
        if len(selIDs) == 0:
            return

        if self._mode == SELECT:
            #Get all selected records
            for sel_id in selIDs:
                self.recordSelected.emit(sel_id)

            rec_selected = QApplication.translate('EntityBrowser',
                                                  'record(s) selected')

            msg = u'{0:d} {1}.'.format(len(selIDs), rec_selected)
            self._notifBar.insertInformationNotification(msg)

    def addModelToView(self, model_obj):
        '''
        Convenience method for adding model info into the view.
        '''
        insertPosition = self._tableModel.rowCount()
        self._tableModel.insertRows(insertPosition, 1)

        for i, attr in enumerate(self._entity_attrs):

            prop_idx = self._tableModel.index(insertPosition, i)
            attr_val = getattr(model_obj, attr)
            '''
            Check if there are display formatters and apply if one exists
            for the given attribute.
            '''
            if attr in self._cell_formatters:
                formatter = self._cell_formatters[attr]
                attr_val = formatter.format_column_value(attr_val)

            self._tableModel.setData(prop_idx, attr_val)
        return insertPosition

    def _model_from_id(self, record_id, row_number):
        '''
        Convenience method that returns the model object based on its ID.
        '''
        dbHandler = self._dbmodel()
        modelObj = dbHandler.queryObject().filter(
            self._dbmodel.id == record_id).first()
        if modelObj is None:
            modelObj = self.child_model[row_number + 1, self.entity]

        return modelObj if not modelObj is None else None
Exemple #3
0
    def _initializeData(self, filtered_records=None):
        '''
        Set table model and load data into it.
        '''
        if self._dbmodel is None:
            msg = QApplication.translate(
                'EntityBrowser',
                'The data model for the entity could not be loaded, \n'
                'please contact your database administrator.'
            )
            QMessageBox.critical(
                self,
                QApplication.translate('EntityBrowser', 'Entity Browser'),
                msg
            )

        else:

            self._init_entity_columns()

            # 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.

            if filtered_records is not None:
                self.current_records = filtered_records.rowcount

            numRecords = self.recomputeRecordCount(init_data=True)

            # Load progress dialog
            progressLabel = QApplication.translate(
                "EntityBrowser", "Fetching Records..."
            )
            progressDialog = QProgressDialog(
                progressLabel, None, 0, numRecords, self
            )

            QApplication.processEvents()
            progressDialog.show()
            progressDialog.setValue(0)

            # Add records to nested list for enumeration in table model
            load_data = True
            if self.plugin is not None:
                if self._entity.name in self.plugin.entity_table_model.keys():
                    if filtered_records is None:
                        self._tableModel = self.plugin.entity_table_model[
                            self._entity.name
                        ]
            if isinstance(self._parent, EntityEditorDialog):
                load_data = True

            if load_data:
                # Only one filter is possible.
                if len(self.filtered_records) > 0:
                    entity_records = self.filtered_records
                else:
                    entity_cls = self._dbmodel()
                    entity_records = entity_cls.queryObject().filter().limit(
                        self.record_limit
                    ).all()

            # if self._tableModel is None:
                entity_records_collection = []
                for i, er in enumerate(entity_records):
                    if i == self.record_limit:
                        break
                    QApplication.processEvents()
                    entity_row_info = []
                    progressDialog.setValue(i)
                    try:
                        for attr in self._entity_attrs:
                            attr_val = getattr(er, attr)

                            # Check if there are display formatters and apply if
                            # one exists for the given attribute.
                            if attr_val is not None: # No need of formatter for None value
                                if attr in self._cell_formatters:
                                    formatter = self._cell_formatters[attr]
                                    attr_val = formatter.format_column_value(attr_val)
                            entity_row_info.append(attr_val)
                    except Exception as ex:
                        QMessageBox.critical(
                            self,
                            QApplication.translate(
                                'EntityBrowser', 'Loading Records'
                            ),
                            unicode(ex.message))
                        return

                    entity_records_collection.append(entity_row_info)


                self._tableModel = BaseSTDMTableModel(
                    entity_records_collection, self._headers, self
                )

                if self.plugin is not None:
                    self.plugin.entity_table_model[self._entity.name] = \
                            self._tableModel

            # Add filter columns
            for header, info in self._searchable_columns.iteritems():
                column_name, index = info['name'], info['header_index']
                if column_name != 'id':
                    self.cboFilterColumn.addItem(header, info)

            #Use sortfilter proxy model for the view
            self._proxyModel = VerticalHeaderSortFilterProxyModel()
            self._proxyModel.setDynamicSortFilter(True)
            self._proxyModel.setSourceModel(self._tableModel)
            self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)

            #USe first column in the combo for filtering
            if self.cboFilterColumn.count() > 0:
                self.set_proxy_model_filter_column(0)

            self.tbEntity.setModel(self._proxyModel)
            if numRecords < self.record_limit:
                self.tbEntity.setSortingEnabled(True)
                self.tbEntity.sortByColumn(1, Qt.AscendingOrder)

            #First (ID) column will always be hidden
            self.tbEntity.hideColumn(0)

            self.tbEntity.horizontalHeader().setResizeMode(QHeaderView.Interactive)

            self.tbEntity.resizeColumnsToContents()

            #Connect signals
            self.connect(self.cboFilterColumn, SIGNAL('currentIndexChanged (int)'), self.onFilterColumnChanged)
            self.connect(self.txtFilterPattern, SIGNAL('textChanged(const QString&)'), self.onFilterRegExpChanged)

            #Select record with the given ID if specified
            if not self._select_item is None:
                self._select_record(self._select_item)

            if numRecords > 0:
                # Set maximum value of the progress dialog
                progressDialog.setValue(numRecords)
            else:
                progressDialog.hide()
Exemple #4
0
    def _initializeData(self):
        '''
        Set table model and load data into it.
        '''
        if self._dbmodel is None:
            msg = QApplication.translate('EntityBrowser', 'The data model for '
                                                          'the entity could '
                                                          'not be loaded, '
                                                          'please contact '
                                                          'your database '
                                                          'administrator.')
            QMessageBox.critical(self, QApplication.translate('EntityBrowser',
                                                              'Entity Browser'),
                                 msg)

            return

        else:

            self._init_entity_columns()
            '''
            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, None, 0, numRecords, self)
            
            entity_cls = self._dbmodel()
            entity_records = entity_cls.queryObject().filter().all()
            
            #Add records to nested list for enumeration in table model
            entity_records_collection = []
            for i,er in enumerate(entity_records):
                entity_row_info = []
                progressDialog.setValue(i)
                try:
                    for attr in self._entity_attrs:
                        attr_val = getattr(er, attr)

                        '''
                        Check if there are display formatters and apply if
                        one exists for the given attribute.
                        '''
                        if attr in self._cell_formatters:
                            formatter = self._cell_formatters[attr]
                            attr_val = formatter.format_column_value(attr_val)

                        entity_row_info.append(attr_val)

                except Exception as ex:
                    QMessageBox.critical(self,
                                         QApplication.translate(
                                             'EntityBrowser',
                                             'Loading Records'
                                         ),
                                         unicode(ex.message))
                    return

                entity_records_collection.append(entity_row_info)
                
            #Set maximum value of the progress dialog
            progressDialog.setValue(numRecords)
        
            self._tableModel = BaseSTDMTableModel(entity_records_collection,
                                                  self._headers, self)

            #Add filter columns
            for header, info in self._searchable_columns.iteritems():
                column_name, index = info['name'], info['header_index']
                if column_name != 'id':
                    self.cboFilterColumn.addItem(header, info)
            
            #Use sortfilter proxy model for the view
            self._proxyModel = VerticalHeaderSortFilterProxyModel()
            self._proxyModel.setDynamicSortFilter(True)
            self._proxyModel.setSourceModel(self._tableModel)
            self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)

            #USe first column in the combo for filtering
            if self.cboFilterColumn.count() > 0:
                self.set_proxy_model_filter_column(0)
            
            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.tbEntity.horizontalHeader().setResizeMode(QHeaderView.Interactive)

            self.tbEntity.resizeColumnsToContents()

            #Connect signals
            self.connect(self.cboFilterColumn, SIGNAL('currentIndexChanged (int)'), self.onFilterColumnChanged)
            self.connect(self.txtFilterPattern, SIGNAL('textChanged(const QString&)'), self.onFilterRegExpChanged)

            #Select record with the given ID if specified
            if not self._select_item is None:
                self._select_record(self._select_item)
Exemple #5
0
class EntityBrowser(SupportsManageMixin, QDialog, Ui_EntityBrowser):
    """
    Dialog for browsing entity records in a table view.
    """

    # 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, entity, parent=None, state=MANAGE, load_records=True, plugin=None):
        QDialog.__init__(self,parent)
        self.setupUi(self)

        # Add maximize buttons
        self.setWindowFlags(
            self.windowFlags() |
            Qt.WindowSystemMenuHint |
            Qt.WindowMaximizeButtonHint
        )

        SupportsManageMixin.__init__(self, state)
        # Init document viewer setup
        self._view_docs_act = None
        viewer_title = QApplication.translate(
            'EntityBrowser',
            'Document Viewer'
        )
        self.doc_viewer_title = u'{0} {1}'.format(
            entity.ui_display(),
            viewer_title
        )
        self._doc_viewer = _EntityDocumentViewerHandler(
            self.doc_viewer_title,
            self
        )
        
        self.load_records = load_records
        #Initialize toolbar
        self.plugin = plugin
        self.tbActions = QToolBar()
        self.tbActions.setObjectName('eb_actions_toolbar')
        self.tbActions.setIconSize(QSize(16, 16))
        self.tbActions.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.vlActions.addWidget(self.tbActions)

        self._entity = entity
        self._dbmodel = entity_model(entity)
        self._state = state
        self._tableModel = None
        self._parent = parent
        self._data_initialized = False
        self._notifBar = NotificationBar(self.vlNotification)
        self._headers = []
        self._entity_attrs = []
        self._cell_formatters = {}
        self.filtered_records = []
        self._searchable_columns = OrderedDict()
        self._show_docs_col = False
        self.child_model = OrderedDict()
        #ID of a record to select once records have been added to the table
        self._select_item = None
        self.current_records = 0

        self.record_limit = self.get_records_limit() #get_entity_browser_record_limit()

        #Enable viewing of supporting documents
        if self.can_view_supporting_documents:
            self._add_view_supporting_docs_btn()

        # self._add_advanced_search_btn()
        #Connect signals
        self.buttonBox.accepted.connect(self.onAccept)
        self.tbEntity.doubleClicked[QModelIndex].connect(self.onDoubleClickView)

    def get_records_limit(self):
        records = get_entity_browser_record_limit()
        if records == 0:
            records = pg_table_count(self.entity.name)
        return records

    def children_entities(self):
        """
        :return: Returns a list of children entities
        that refer to the main entity as the parent.
        :rtype: list
        """
        return [ch for ch in self._entity.children()
                if ch.TYPE_INFO == Entity.TYPE_INFO]

    @property
    def entity(self):
        """
        :return: Returns the Entity object used in this browser.
        :rtype: Entity
        """
        return self._entity

    @property
    def can_view_supporting_documents(self):
        """
        :return: True if the browser supports the viewing of supporting
        documents.
        :rtype: bool
        """
        test_ent_obj = self._dbmodel()

        if self._entity.supports_documents \
                and hasattr(test_ent_obj, 'documents'):
            return True

        return False

    def _add_view_supporting_docs_btn(self):
        #Add button for viewing supporting documents if supported
        view_docs_str = QApplication.translate(
            'EntityBrowser',
            'View Documents'
        )
        self._view_docs_act = QAction(
            QIcon(':/plugins/stdm/images/icons/document.png'),
            view_docs_str,
            self
        )

        #Connect signal for showing document viewer
        self._view_docs_act.triggered.connect(self.on_load_document_viewer)

        self.tbActions.addAction(self._view_docs_act)


    # def _add_advanced_search_btn(self):
    #     #Add button for viewing supporting documents if supported
    #     search_str = QApplication.translate(
    #         'EntityBrowser',
    #         'Advanced Search'
    #     )
    #     self._search_act = QAction(
    #         QIcon(':/plugins/stdm/images/icons/advanced_search.png'),
    #         search_str,
    #         self
    #     )

    #     #Connect signal for showing document viewer
    #     self._search_act.triggered.connect(self.on_advanced_search)

    #     self.tbActions.addAction(self._search_act)

    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 set_selection_record_id(self, id):
        """
        Set the ID of a record to be selected only once all records have been
        added to the table view.
        :param id: Record id to be selected.
        :type id: int
        """
        self._select_item = id

    def title(self):
        '''
        Set the title of the entity browser dialog.
        Protected method to be overridden by subclasses.
        '''
        records = QApplication.translate('EntityBrowser', 'Records')
        if self._entity.label != '':
            title = self._entity.label
        else:
            title = self._entity.ui_display()

        return u'{} {}'.format(title, records)

    def setCellFormatters(self,formattermapping):
        '''
        Dictionary of attribute mappings and corresponding functions for
        formatting the attribute value to the display value.
        '''
        self._cell_formatters = formattermapping

    def addCellFormatter(self,attributeName,formatterFunc):
        '''
        Add a new cell formatter configuration to the collection
        '''
        self._cell_formatters[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(unicode(self.title()))

        if self._data_initialized:
            return
        try:
            if not self._dbmodel is None:
                # cProfile.runctx('self._initializeData()', globals(), locals())
                self._initializeData()

        except Exception as ex:
            pass

        self._data_initialized = True

    def hideEvent(self,hideEvent):
        '''
        Override event which just sets a flag to indicate that the data records have already been
        initialized.
        '''
        pass

    def clear_selection(self):
        """
        Deselects all selected items in the table view.
        """
        self.tbEntity.clearSelection()

    def clear_notifications(self):
        """
        Clears all notifications messages in the dialog.
        """
        self._notifBar.clear()

    def recomputeRecordCount(self, init_data=False):
        '''
        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()
        if init_data:
            if self.current_records < 1:
                if numRecords > self.record_limit:
                    self.current_records = self.record_limit
                else:
                    self.current_records = numRecords

        rowStr = QApplication.translate('EntityBrowser', 'row') \
            if numRecords == 1 \
            else QApplication.translate('EntityBrowser', 'rows')
        showing = QApplication.translate('EntityBrowser', 'Showing')
        windowTitle = u"{0} - {1} {2} of {3} {4}".format(
            self.title(), showing, self.current_records, numRecords, rowStr
        )

        self.setWindowTitle(windowTitle)

        return numRecords

    def _init_entity_columns(self):
        """
        Asserts if the entity columns actually do exist in the database. The
        method also initializes the table headers, entity column and cell
        formatters.
        """
        self._headers[:] = []
        table_name = self._entity.name
        columns = table_column_names(table_name)
        missing_columns = []

        header_idx = 0

        #Iterate entity column and assert if they exist

        for c in self._entity.columns.values():


            # Exclude geometry columns
            if isinstance(c, GeometryColumn):
                continue

            # Do not include virtual columns in list of missing columns
            if not c.name in columns and not isinstance(c, VirtualColumn):
                missing_columns.append(c.name)

            else:
                header = c.ui_display()
                self._headers.append(header)
                col_name = c.name

                '''
                If it is a virtual column then use column name as the header
                but fully qualified column name (created by SQLAlchemy
                relationship) as the entity attribute name.
                '''

                if isinstance(c, MultipleSelectColumn):
                    col_name = c.model_attribute_name

                self._entity_attrs.append(col_name)

                # Get widget factory so that we can use the value formatter
                w_factory = ColumnWidgetRegistry.factory(c.TYPE_INFO)
                if not w_factory is None:
                    formatter = w_factory(c)
                    self._cell_formatters[col_name] = formatter

                # Set searchable columns
                if c.searchable:
                    self._searchable_columns[c.ui_display()] = {
                        'name': c.name,
                        'header_index': header_idx
                    }

                header_idx += 1

        if len(missing_columns) > 0:
            msg = QApplication.translate(
                'EntityBrowser',
                u'The following columns have been defined in the '
                u'configuration but are missing in corresponding '
                u'database table, please re-run the configuration wizard '
                u'to create them.\n{0}'.format(
                    '\n'.join(missing_columns)
                )
            )

            QMessageBox.warning(
                self,
                QApplication.translate('EntityBrowser','Entity Browser'),
                msg
            )

    def _select_record(self, id):
        #Selects record with the given ID.
        if id is None:
            return

        m = self.tbEntity.model()
        s = self.tbEntity.selectionModel()

        start_idx = m.index(0, 0)
        idxs = m.match(
            start_idx,
            Qt.DisplayRole,
            id,
            1,
            Qt.MatchExactly
        )

        if len(idxs) > 0:
            sel_idx = idxs[0]
             #Select item
            s.select(
                sel_idx,
                QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows
            )

    # def on_advanced_search(self):
    #     search = AdvancedSearch(self._entity, parent=self)
    #     search.show()

    def on_load_document_viewer(self):
        #Slot raised to show the document viewer for the selected entity
        sel_rec_ids = self._selected_record_ids()

        if len(sel_rec_ids) == 0:
            return

        #Get document objects
        ent_obj = self._dbmodel()

        for sel_id in sel_rec_ids:
            er = ent_obj.queryObject().filter(self._dbmodel.id == sel_id).first()
            if not er is None:
                docs = er.documents

                #Notify there are no documents for the selected doc
                if len(docs) == 0:
                    msg = QApplication.translate(
                        'EntityBrowser',
                        'There are no supporting documents for the selected record.'
                    )

                    QMessageBox.warning(
                        self,
                        self.doc_viewer_title,
                        msg
                    )

                    continue

                self._doc_viewer.load(docs)

    def _initializeData(self, filtered_records=None):
        '''
        Set table model and load data into it.
        '''
        if self._dbmodel is None:
            msg = QApplication.translate(
                'EntityBrowser',
                'The data model for the entity could not be loaded, \n'
                'please contact your database administrator.'
            )
            QMessageBox.critical(
                self,
                QApplication.translate('EntityBrowser', 'Entity Browser'),
                msg
            )

        else:

            self._init_entity_columns()

            # 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.

            if filtered_records is not None:
                self.current_records = filtered_records.rowcount

            numRecords = self.recomputeRecordCount(init_data=True)

            # Load progress dialog
            progressLabel = QApplication.translate(
                "EntityBrowser", "Fetching Records..."
            )
            progressDialog = QProgressDialog(
                progressLabel, None, 0, numRecords, self
            )

            QApplication.processEvents()
            progressDialog.show()
            progressDialog.setValue(0)

            # Add records to nested list for enumeration in table model
            load_data = True
            if self.plugin is not None:
                if self._entity.name in self.plugin.entity_table_model.keys():
                    if filtered_records is None:
                        self._tableModel = self.plugin.entity_table_model[
                            self._entity.name
                        ]
            if isinstance(self._parent, EntityEditorDialog):
                load_data = True

            if load_data:
                # Only one filter is possible.
                if len(self.filtered_records) > 0:
                    entity_records = self.filtered_records
                else:
                    entity_cls = self._dbmodel()
                    entity_records = entity_cls.queryObject().filter().limit(
                        self.record_limit
                    ).all()

            # if self._tableModel is None:
                entity_records_collection = []
                for i, er in enumerate(entity_records):
                    if i == self.record_limit:
                        break
                    QApplication.processEvents()
                    entity_row_info = []
                    progressDialog.setValue(i)
                    try:
                        for attr in self._entity_attrs:
                            attr_val = getattr(er, attr)

                            # Check if there are display formatters and apply if
                            # one exists for the given attribute.
                            if attr_val is not None: # No need of formatter for None value
                                if attr in self._cell_formatters:
                                    formatter = self._cell_formatters[attr]
                                    attr_val = formatter.format_column_value(attr_val)
                            entity_row_info.append(attr_val)
                    except Exception as ex:
                        QMessageBox.critical(
                            self,
                            QApplication.translate(
                                'EntityBrowser', 'Loading Records'
                            ),
                            unicode(ex.message))
                        return

                    entity_records_collection.append(entity_row_info)


                self._tableModel = BaseSTDMTableModel(
                    entity_records_collection, self._headers, self
                )

                if self.plugin is not None:
                    self.plugin.entity_table_model[self._entity.name] = \
                            self._tableModel

            # Add filter columns
            for header, info in self._searchable_columns.iteritems():
                column_name, index = info['name'], info['header_index']
                if column_name != 'id':
                    self.cboFilterColumn.addItem(header, info)

            #Use sortfilter proxy model for the view
            self._proxyModel = VerticalHeaderSortFilterProxyModel()
            self._proxyModel.setDynamicSortFilter(True)
            self._proxyModel.setSourceModel(self._tableModel)
            self._proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)

            #USe first column in the combo for filtering
            if self.cboFilterColumn.count() > 0:
                self.set_proxy_model_filter_column(0)

            self.tbEntity.setModel(self._proxyModel)
            if numRecords < self.record_limit:
                self.tbEntity.setSortingEnabled(True)
                self.tbEntity.sortByColumn(1, Qt.AscendingOrder)

            #First (ID) column will always be hidden
            self.tbEntity.hideColumn(0)

            self.tbEntity.horizontalHeader().setResizeMode(QHeaderView.Interactive)

            self.tbEntity.resizeColumnsToContents()

            #Connect signals
            self.connect(self.cboFilterColumn, SIGNAL('currentIndexChanged (int)'), self.onFilterColumnChanged)
            self.connect(self.txtFilterPattern, SIGNAL('textChanged(const QString&)'), self.onFilterRegExpChanged)

            #Select record with the given ID if specified
            if not self._select_item is None:
                self._select_record(self._select_item)

            if numRecords > 0:
                # Set maximum value of the progress dialog
                progressDialog.setValue(numRecords)
            else:
                progressDialog.hide()

    def _header_index_from_filter_combo_index(self, idx):
        col_info = self.cboFilterColumn.itemData(idx)

        return col_info['name'], col_info['header_index']

    def set_proxy_model_filter_column(self, index):
        #Set the filter column for the proxy model using the combo index
        name, header_idx = self._header_index_from_filter_combo_index(index)
        self._proxyModel.setFilterKeyColumn(header_idx)

    def onFilterColumnChanged(self, index):
        '''
        Set the filter column for the proxy model.
        '''
        self.set_proxy_model_filter_column(index)

    def _onFilterRegExpChanged(self,text):
        cProfile.runctx('self._onFilterRegExpChanged(text)', globals(), locals())

    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 _selected_record_ids(self):
        '''
        Get the IDs of the selected row in the table view.
        '''
        self._notifBar.clear()

        selected_ids = []
        sel_row_indices = self.tbEntity.selectionModel().selectedRows(0)

        if len(sel_row_indices) == 0:
            msg = QApplication.translate("EntityBrowser",
                                         "Please select a record from the table.")

            self._notifBar.insertWarningNotification(msg)

            return selected_ids

        for proxyRowIndex in sel_row_indices:
            #Get the index of the source or else the row items will have unpredictable behavior
            row_index = self._proxyModel.mapToSource(proxyRowIndex)
            entity_id = row_index.data(Qt.DisplayRole)
            selected_ids.append(entity_id)

        return selected_ids

    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._selected_record_ids()
        if len(selIDs) == 0:
            return

        if self._mode == SELECT:
            #Get all selected records
            for sel_id in selIDs:
                self.recordSelected.emit(sel_id)

            rec_selected = QApplication.translate(
                'EntityBrowser',
                'record(s) selected'
            )

            msg = u'{0:d} {1}.'.format(len(selIDs), rec_selected)
            self._notifBar.insertInformationNotification(msg)

    def addModelToView(self, model_obj):
        '''
        Convenience method for adding model info into the view.
        '''
        insertPosition = self._tableModel.rowCount()
        self._tableModel.insertRows(insertPosition, 1)

        for i, attr in enumerate(self._entity_attrs):

            prop_idx = self._tableModel.index(insertPosition, i)
            attr_val = getattr(model_obj, attr)

            '''
            Check if there are display formatters and apply if one exists
            for the given attribute.
            '''
            if attr in self._cell_formatters:
                formatter = self._cell_formatters[attr]
                attr_val = formatter.format_column_value(attr_val)

            self._tableModel.setData(prop_idx, attr_val)
        return insertPosition

    def _model_from_id(self, record_id, row_number):
        '''
        Convenience method that returns the model object based on its ID.
        '''
        dbHandler = self._dbmodel()
        modelObj = dbHandler.queryObject().filter(
            self._dbmodel.id == record_id
        ).first()

        if modelObj is None:
            modelObj = self.child_model[row_number+1]

        return modelObj