Sunday 8 August 2021

QSqlRelationalDelegate displays foreign_key - id of the related record instead of name/value from Combobox

I am trying to add new rows to QSqlRelationalModel which is represented in QTableView.

I have set proper QSqlRelationalDelegate and proper QSqlRelations in the model. Displaying existing data from the database works fine. Columns with related data change to Comboboxes and I can choose options from the related tables.

However, when I try to create a new record by adding a row to the model, Comboboxes allow me to choose the proper value from the dropdown list, but after choosing it, the value changes to the ID of the related record as if no relational delegate was set.

I am also using QSortFilterProxyModel to enable sorting. Therefore, I have overriden the QSqlRelationalDelegate to address the issue with changing indexes.

Here is the main.py that draws the widgets:


import sys

from PyQt6 import QtCore, QtWidgets
from PyQt6.QtCore import QModelIndex, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel, QSqlRelationalTableModel, QSqlRelation, QSqlQuery
from PyQt6.QtWidgets import QPushButton

from custom_delegates import SortSupportQSqlRelationalDelegate
from custom_qt_models import EmptyTextReplacerQSortFilterProxyModel


class Ui_main(object):
    def setupUi(self, main):
        main.setObjectName("main")
        main.resize(781, 652)

        self.verticalLayoutWidget = QtWidgets.QWidget(main)
        self.verticalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 761, 631))
        self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")

        # Replace values with your database configurations
        database = QSqlDatabase.addDatabase('QPSQL')
        database.setHostName('localhost')
        database.setPort(5432)
        database.setDatabaseName('ars_default')
        database.setUserName('hamlet')
        database.setPassword('qwerty')
        database.open()

        button_add = QPushButton("AddRow")
        button_add.clicked.connect(self.addRow)
        self.verticalLayout.addWidget(button_add)

        self.tableView = QtWidgets.QTableView(self.verticalLayoutWidget)
        self.tableView.setObjectName("tableView")
        self.tableView.verticalHeader().setVisible(False)
        self.verticalLayout.addWidget(self.tableView)

        self.table_model = QSqlRelationalTableModel(main, database)
        self.table_model.setJoinMode(QSqlRelationalTableModel.JoinMode.LeftJoin)
        table_name = 'book_of_accounts'
        self.table_name = table_name
        self.table_model.setTable(table_name)

        self.set_relations()

        m = EmptyTextReplacerQSortFilterProxyModel()
        m.setDynamicSortFilter(True)
        m.setSourceModel(self.table_model)

        self.tableView.setModel(m)

        self.tableView.setSortingEnabled(True)
        self.tableView.setItemDelegate(SortSupportQSqlRelationalDelegate(self.tableView))

        self.table_model.setEditStrategy(QSqlTableModel.EditStrategy.OnFieldChange)
        self.table_model.select()

        self.tableView.hideColumn(0)

        self.retranslateUi(main)
        QtCore.QMetaObject.connectSlotsByName(main)

    def retranslateUi(self, main):
        _translate = QtCore.QCoreApplication.translate
        main.setWindowTitle(_translate("main", "main"))

    def addRow(self):
        self.tableView.sortByColumn(-1, Qt.SortOrder.AscendingOrder)
        count = self.table_model.rowCount(QModelIndex())
        self.table_model.insertRows(count, 1)
        self.tableView.scrollToBottom()

        next_id = self.get_next_id(self.table_name)
        # A workaround to the problem described in my other so question [link is under this code block]
        self.table_model.setData(self.table_model.index(count, 0), next_id, Qt.ItemDataRole.EditRole)
        self.tableView.updateGeometry()
        self.tableView.selectRow(count)

    def get_next_id(self, table_name):
        max_id = -2
        query = QSqlQuery()
        query.exec(f"SELECT MAX(id) FROM {table_name}")
        if query.next():
            print(f"{query.value(0)=}")
            max_id = query.value(0)

        return max_id + 1

    def set_relations(self):
        self.table_model.setRelation(4, QSqlRelation('account_type', 'id', 'name'))
        self.table_model.setRelation(7, QSqlRelation('subconto1', 'id', 'name'))
        self.table_model.setRelation(8, QSqlRelation('subconto2', 'id', 'name'))
        self.table_model.setRelation(9, QSqlRelation('subconto3', 'id', 'name'))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = QtWidgets.QWidget()
    window = Ui_main()
    window.setupUi(main_window)
    main_window.show()
    sys.exit(app.exec())

Here is the link to the question mentioned in the comment above

The implementation of the Custom delegate (SortSupportQSqlRelationalDelegate.py):


from PyQt6 import QtGui, QtCore
from PyQt6.QtCore import QSortFilterProxyModel
from PyQt6.QtSql import QSqlRelationalDelegate, QSqlRelationalTableModel
from PyQt6.QtWidgets import QComboBox, QStyleOptionViewItem


class SortSupportQSqlRelationalDelegate(QSqlRelationalDelegate):
    """
    When using QSortFilterProxyModel along with QSqlRelationalModel, columns holding relational data need to
    change indexes to source model indexes. That is why override createEditor, setEditorData and setModelData.
    """
    def __init__(self, *args, **kwargs):
        super(SortSupportQSqlRelationalDelegate, self).__init__(*args, **kwargs)

    def createEditor(self, parent, option, index):
        proxy = index.model()
        print(type(proxy))
        if not isinstance(proxy, QSqlRelationalTableModel):
            base_index = proxy.mapToSource(index)
        else:
            base_index = index
        editor = None

        if index.column() in (4, 7, 8, 9):
            editor = super(SortSupportQSqlRelationalDelegate, self).createEditor(parent, option, base_index)
        else:
            editor = super(SortSupportQSqlRelationalDelegate, self).createEditor(parent, option, index)

        return editor

    def setEditorData(self, editor, index):

        if index.column() in (4, 7, 8, 9):
            proxy = index.model()
            base_index = proxy.mapToSource(index)
            return super(SortSupportQSqlRelationalDelegate, self).setEditorData(editor, base_index)
        else:
            return super(SortSupportQSqlRelationalDelegate, self).setEditorData(editor, index)

    def setModelData(self, editor: QComboBox, model: QSortFilterProxyModel, index):

        if index.column() in (4, 7, 8, 9):
            base_model = model.sourceModel()
            base_index = model.mapToSource(index)

            # This shows the row number instead of value when adding new row
            print(base_model.data(base_index))

            return super(SortSupportQSqlRelationalDelegate, self).setModelData(editor, base_model, base_index)
        else:
            return super(SortSupportQSqlRelationalDelegate, self).setModelData(editor, model, index)

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
        """
        Align text in cells except for column 2
        """
        if index.column() != 2:
            option.displayAlignment = QtCore.Qt.AlignmentFlag.AlignCenter

        return super().paint(painter, option, index)

I also have used a custom QSortFilterProxyModel to replace some displayed texts. This code does not affect the problem. I have tried to use ordinary QSortFilterProxyModel and even removed it, but the issue remained.

EmptyTextReplacerQSortFilterProxyModel.py:


from PyQt6.QtCore import Qt, QSortFilterProxyModel


class EmptyTextReplacerQSortFilterProxyModel(QSortFilterProxyModel):
    """
    This proxy model makes the '< Ничего >' strings to be displayed as empty strings.
    """
    def __init__(self, *args, **kwargs):
        super(EmptyTextReplacerQSortFilterProxyModel, self).__init__(*args, **kwargs)

    def data(self, index, role=None):

        if super(EmptyTextReplacerQSortFilterProxyModel, self).data(index, Qt.ItemDataRole.DisplayRole) != '< Ничего >':
            return super(EmptyTextReplacerQSortFilterProxyModel, self).data(index, role)

        if role == Qt.ItemDataRole.EditRole or role != Qt.ItemDataRole.DisplayRole:
            return super(EmptyTextReplacerQSortFilterProxyModel, self).data(index, role)

        return ""

This problem might be related to an unanswered 6-years-old so question.



from QSqlRelationalDelegate displays foreign_key - id of the related record instead of name/value from Combobox

No comments:

Post a Comment