qtableview fails to display the newly inserted row

Viewed 327

When using QtableView + QSqlTableModel combination, QtableView fails to properly display the inserted row after insertion of a new one.

The id field is hidden. The EditStrategy is OnFieldChange. There is a button to insert a new empty row at the end of the model. After the user fills in all fields and hits enter or clicks on other rows, the expected behavior is for the row to be submitted to the database and the view keeping the new row. However, the QSqlTableModel succeeds to insert the data into the database table, but the tableview's newly inserted row becomes empty and fields uneditable.

The behavior changes to the desired one if the id field is not hidden and is explicitly provided. Yet if the id is not hidden and not populated manually, the behaviour fails again as mentioned above.

Why this happens?

Here is the minimum reproducible code. You will have to change database configuration fileds. And connect to the table with autoincrement id:

import sys

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QModelIndex
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
from PyQt5.QtWidgets import QPushButton


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

        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
        # But the table you refer to must have autoincremented primary key
        database = QSqlDatabase.addDatabase('QPSQL')
        database.setHostName('localhost')
        database.setPort(5432)
        database.setDatabaseName('some_database')
        database.setUserName('some_user')
        database.setPassword('some_password')
        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 = QSqlTableModel(self.tableView, database)
        table_name = 'some_table'
        self.table_model.setTable(table_name)

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

        self.tableView.setModel(self.table_model)
        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):
        count = self.table_model.rowCount(QModelIndex())
        self.table_model.insertRow(count)
        self.tableView.scrollToBottom()

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_())

Update:

The temporary workaround I am using right now is finding max id from the fetched table and append max_id + 1 to the newly inserted row. Thus, the row has an id on submission to the database. However, this seems to be not the right way as the database handles primary keys itself...

Update 2: Here is how my database table looks like:

Design:

book_of_accounts: table
+ columns
    id: bigint NN default nextval('book_of_accounts_id_seq'::regclass)
    code: varchar(256) NN
    name: varchar(1024) NN
    account_type: varchar(256) NN
    quantitative: boolean NN
    monetary: boolean NN
    subconoto_1: varchar(256)
    subconoto_2: varchar(256)
    subconoto_3: varchar(256)
+ indices
    book_of_accounts_pkey: unique (id)
    bookofaccounts_code: unique (code)
    bookofaccounts_name: index (name)
+ keys
    book_of_accounts_pkey: PK (id)

SQL

create table if not exists book_of_accounts
(
    id bigserial not null
        constraint book_of_accounts_pkey
        primary key,
    code varchar(256) not null,
    name varchar(1024) not null,
    account_type varchar(256) not null,
    quantitative boolean not null,
    monetary boolean not null,
    subconoto_1 varchar(256),
    subconoto_2 varchar(256),
    subconoto_3 varchar(256)
);

alter table book_of_accounts owner to hamlet;

create unique index if not exists bookofaccounts_code
    on book_of_accounts (code);

create index if not exists bookofaccounts_name
    on book_of_accounts (name);
1 Answers

OK, so the problem seems to be that (quoting from Qt docs):

Note: To prevent inserting only partly initialized rows into the database, OnFieldChange will behave like OnRowChange for newly inserted rows.

To make the table update and display the newly inserted row, you can call the model's submitAll() method on data change.

Complete updated code (updates marked in comments):

import sys

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QModelIndex
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
from PyQt5.QtWidgets import QPushButton

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

        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
        # But the table you refer to must have autoincremented primary key
        database = QSqlDatabase.addDatabase('QPSQL')
        database.setHostName('localhost')
        database.setPort(5432)
        database.setDatabaseName('dbname')
        database.setUserName('postgres')
        database.setPassword('password')
        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 = QSqlTableModel(self.tableView, database)
        table_name = 'units'
        self.table_model.setTable(table_name)

        self.table_model.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.table_model.select()
        ## ========== ADDED ========== ##
        self.table_model.sort(0, 0)    # DEFAULT SORT TO SEE NEW ITEMS AT END       
        self.table_model.dataChanged.connect(self.on_dataChanged) # CONNECT DATA CHANGE SIGNAL TO CUSTOM SLOT
        ## =========================== ##

        self.tableView.setModel(self.table_model)
        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):
        count = self.table_model.rowCount(QModelIndex())
        self.table_model.insertRow(count)
        self.tableView.scrollToBottom()

    ## ========== ADDED ========== ##
    # SLOT FIRED WHEN DATA CHANGES
    def on_dataChanged(self, topLeft, bottomRight, roles):
        self.table_model.submitAll()  # COMMIT TO DB
        self.table_model.select()     # RESELECT FROM DB
        self.table_model.sort(0, 0)   # SORT
    ## =========================== ##

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_())
Related