StatusQ: Add generic LeftJoinModel model mimicking SQL Left Join

Closes: #12502
This commit is contained in:
Michał Cieślak 2023-10-24 11:08:30 +02:00 committed by Michał
parent 628d9cdd31
commit ffadd7522c
7 changed files with 1215 additions and 0 deletions

View File

@ -0,0 +1,283 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
Item {
id: root
readonly property var colors: [
"red", "green", "purple", "orange","gray", "ping", "brown", "blue"
]
ListModel {
id: leftModel
ListElement {
title: "Token 1"
communityId: "1"
}
ListElement {
title: "Token 2"
communityId: "1"
}
ListElement {
title: "Token 3"
communityId: "2"
}
ListElement {
title: "Token 4"
communityId: "3"
}
ListElement {
title: "Token 5"
communityId: ""
}
ListElement {
title: "Token 6"
communityId: "1"
}
}
ListModel {
id: rightModel
ListElement {
communityId: "1"
name: "Community 1"
color: "red"
}
ListElement {
communityId: "2"
name: "Community 2"
color: "green"
}
ListElement {
communityId: "3"
name: "Community 3"
color: "blue"
}
}
LeftJoinModel {
id: leftJoinModel
leftModel: leftModel
rightModel: rightModel
joinRole: "communityId"
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 40
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
border.color: "gray"
ListView {
anchors.fill: parent
model: leftModel
header: Label {
height: implicitHeight * 2
text: `Left model (${leftModel.count})`
font.bold: true
verticalAlignment: Text.AlignVCenter
}
ScrollBar.vertical: ScrollBar {}
delegate: Label {
width: ListView.view.width
text: `${model.title}, community id: ${model.communityId || "-"}`
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
border.color: "gray"
ListView {
anchors.fill: parent
model: rightModel
header: Label {
height: implicitHeight * 2
text: `Right model (${rightModel.count})`
font.bold: true
verticalAlignment: Text.AlignVCenter
}
ScrollBar.vertical: ScrollBar {}
delegate: RowLayout {
width: ListView.view.width - 10
Label {
Layout.fillWidth: true
text: `${model.name}, community id: ${model.communityId}`
}
Rectangle {
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
color: model.color
MouseArea {
anchors.fill: parent
onClicked: {
const colorIndex = root.colors.indexOf(
model.color)
const nextIndex = (colorIndex + 1)
% root.colors.length
rightModel.setProperty(index, "color",
root.colors[nextIndex])
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
border.color: "gray"
ListView {
id: leftJoinListView
anchors.fill: parent
model: leftJoinModel
header: Label {
height: implicitHeight * 2
text: `Left Join model (${leftJoinListView.count})`
font.bold: true
verticalAlignment: Text.AlignVCenter
}
ScrollBar.vertical: ScrollBar {}
delegate: RowLayout {
id: row
width: ListView.view.width - 10
readonly property bool hasCommunity: model.communityId.length
Label {
Layout.fillWidth: true
text: model.title + (row.hasCommunity
? `, community id: ${model.communityId}, name: ${model.name}`
: "")
}
Rectangle {
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
color: model.color || "transparent"
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Left model count: " + leftModel.count
}
Label {
text: "Right model count: " + rightModel.count
}
Button {
text: "add 100 left model items"
onClicked: {
for (let i = 0; i < 100; i++) {
const count = leftModel.count
const entry = {
title: "Token " + (count + 1),
communityId: (Math.floor(Math.random() * rightModel.count) + 1).toString()
}
leftModel.append(entry)
}
}
}
Button {
text: "add 10 right model items"
onClicked: {
for (let i = 0; i < 10; i++) {
const count = rightModel.count
const entry = {
communityId: count.toString(),
name: "Community " + count,
color: root.colors[Math.floor(Math.random() * colors.length)]
}
rightModel.append(entry)
}
}
}
Button {
text: "shuffle"
onClicked: {
const count = leftModel.count
const iterations = count / 2
for (let i = 0; i < iterations; i++) {
leftModel.move(Math.floor(Math.random() * (count - 1)),
Math.floor(Math.random() * (count - 1)),
Math.floor(Math.random() * 2) + 1)
}
}
}
}
}
}
// category: Models

View File

@ -89,6 +89,7 @@ endif()
add_library(StatusQ SHARED
${STATUSQ_QRC_COMPILED}
include/StatusQ/QClipboardProxy.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelutilsinternal.h
include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h
@ -97,6 +98,7 @@ add_library(StatusQ SHARED
include/StatusQ/statuswindow.h
include/StatusQ/stringutilsinternal.h
src/QClipboardProxy.cpp
src/leftjoinmodel.cpp
src/modelutilsinternal.cpp
src/permissionutilsinternal.cpp
src/plugin.cpp

View File

@ -0,0 +1,58 @@
#pragma once
#include <QIdentityProxyModel>
class LeftJoinModel : public QIdentityProxyModel
{
Q_OBJECT
Q_PROPERTY(QAbstractItemModel* leftModel READ leftModel
WRITE setLeftModel NOTIFY leftModelChanged)
Q_PROPERTY(QAbstractItemModel* rightModel READ rightModel
WRITE setRightModel NOTIFY rightModelChanged)
Q_PROPERTY(QString joinRole READ joinRole
WRITE setJoinRole NOTIFY joinRoleChanged)
public:
explicit LeftJoinModel(QObject* parent = nullptr);
void setLeftModel(QAbstractItemModel* model);
QAbstractItemModel* leftModel() const;
void setRightModel(QAbstractItemModel* model);
QAbstractItemModel* rightModel() const;
void setJoinRole(const QString& joinRole);
const QString& joinRole() const;
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex& index, int role) const override;
void setSourceModel(QAbstractItemModel* newSourceModel) override;
signals:
void leftModelChanged();
void rightModelChanged();
void joinRoleChanged();
private:
void initializeIfReady();
void initialize();
int m_rightModelRolesOffset = 0;
QHash<int, QByteArray> m_roleNames;
QVector<int> m_joinedRoles;
QString m_joinRole;
int m_leftModelJoinRole = 0;
int m_rightModelJoinRole = 0;
QAbstractItemModel* m_leftModel = nullptr;
QAbstractItemModel* m_rightModel = nullptr;
bool m_leftModelDestroyed = false;
bool m_rightModelDestroyed = false;
mutable QPersistentModelIndex m_lastUsedRightModelIndex;
};

View File

@ -0,0 +1,240 @@
#include "StatusQ/leftjoinmodel.h"
#include <QDebug>
#include <algorithm>
LeftJoinModel::LeftJoinModel(QObject* parent)
: QIdentityProxyModel{parent}
{
}
void LeftJoinModel::initializeIfReady()
{
if (m_leftModel && m_rightModel && !m_joinRole.isEmpty()
&& !m_leftModel->roleNames().empty()
&& !m_rightModel->roleNames().empty())
initialize();
}
void LeftJoinModel::initialize()
{
auto leftRoleNames = m_leftModel->roleNames();
auto rightRoleNames = m_rightModel->roleNames();
if (leftRoleNames.isEmpty() || rightRoleNames.isEmpty()) {
qWarning() << "Both left and right models have to contain some roles!";
return;
}
auto leftModelJoinRoleList = leftRoleNames.keys(m_joinRole.toUtf8());
auto rightModelJoinRoleList = rightRoleNames.keys(m_joinRole.toUtf8());
if (leftModelJoinRoleList.size() != 1
|| rightModelJoinRoleList.size() != 1) {
qWarning().noquote() << QString("Both left and right models have to "
"contain join role %1!").arg(m_joinRole);
return;
}
m_leftModelJoinRole = leftModelJoinRoleList.at(0);
m_rightModelJoinRole = rightModelJoinRoleList.at(0);
auto leftRoles = leftRoleNames.keys();
auto maxLeftRole = std::max_element(leftRoles.cbegin(), leftRoles.cend());
auto rightRolesOffset = *maxLeftRole + 1;
auto roleNames = leftRoleNames;
auto i = rightRoleNames.constBegin();
while (i != rightRoleNames.constEnd()) {
if (i.value() != m_joinRole) {
auto roleWithOffset = i.key() + rightRolesOffset;
roleNames.insert(roleWithOffset, i.value());
m_joinedRoles.append(roleWithOffset);
}
++i;
}
m_rightModelRolesOffset = rightRolesOffset;
m_roleNames = roleNames;
connect(m_rightModel, &QAbstractItemModel::dataChanged, this,
[this](auto& topLeft, auto& bottomRight, auto& roles) {
QVector<int> rolesTranslated;
if (roles.contains(m_rightModelJoinRole)) {
rolesTranslated = m_joinedRoles;
} else {
rolesTranslated = roles;
for (auto& role : rolesTranslated)
role += m_rightModelRolesOffset;
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0), rolesTranslated);
});
disconnect(m_leftModel, &QAbstractItemModel::rowsInserted,
this, &LeftJoinModel::initializeIfReady);
disconnect(m_rightModel, &QAbstractItemModel::rowsInserted,
this, &LeftJoinModel::initializeIfReady);
auto emitJoinedRolesChanged = [this] {
emit dataChanged(index(0, 0), index(rowCount() - 1, 0), m_joinedRoles);
};
connect(m_rightModel, &QAbstractItemModel::rowsRemoved, this,
emitJoinedRolesChanged);
connect(m_rightModel, &QAbstractItemModel::rowsInserted, this,
emitJoinedRolesChanged);
connect(m_rightModel, &QAbstractItemModel::modelReset, this,
emitJoinedRolesChanged);
connect(m_rightModel, &QAbstractItemModel::layoutChanged, this,
emitJoinedRolesChanged);
connect(this, &QAbstractItemModel::dataChanged, this,
[this](auto& topLeft, auto& bottomRight, auto& roles) {
if (roles.contains(m_leftModelJoinRole))
emit dataChanged(topLeft, bottomRight, m_joinedRoles);
});
QIdentityProxyModel::setSourceModel(m_leftModel);
}
QVariant LeftJoinModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return {};
auto idx = m_leftModel->index(index.row(), index.column());
if (role < m_rightModelRolesOffset)
return m_leftModel->data(idx, role);
if (m_rightModelDestroyed)
return {};
QVariant joinRoleLeftValue = m_leftModel->data(idx, m_leftModelJoinRole);
if (m_lastUsedRightModelIndex.isValid()
&& m_rightModel->data(m_lastUsedRightModelIndex,
m_rightModelJoinRole) == joinRoleLeftValue)
{
return m_rightModel->data(m_lastUsedRightModelIndex,
role - m_rightModelRolesOffset);
}
int rightModelCount = m_rightModel->rowCount();
for (int i = 0; i < rightModelCount; i++) {
auto rightModelIdx = m_rightModel->index(i, 0);
auto rightJointRoleValue = m_rightModel->data(rightModelIdx,
m_rightModelJoinRole);
if (joinRoleLeftValue == rightJointRoleValue) {
m_lastUsedRightModelIndex = rightModelIdx;
return m_rightModel->data(rightModelIdx,
role - m_rightModelRolesOffset);
}
}
return {};
}
void LeftJoinModel::setLeftModel(QAbstractItemModel* model)
{
if (m_leftModel == model)
return;
if (m_leftModel != nullptr || m_leftModelDestroyed) {
qWarning("Changing left model is not supported!");
return;
}
m_leftModel = model;
// Some models may have roles undefined until first row is inserted,
// like ListModel, therefore in such cases initialization must be deferred
// until first insertion.
connect(m_leftModel, &QAbstractItemModel::rowsInserted,
this, &LeftJoinModel::initializeIfReady);
connect(m_leftModel, &QObject::destroyed, this, [this] {
this->m_leftModel = nullptr;
this->m_leftModelDestroyed = true;
});
emit leftModelChanged();
initializeIfReady();
}
QAbstractItemModel* LeftJoinModel::leftModel() const
{
return m_leftModel;
}
void LeftJoinModel::setRightModel(QAbstractItemModel* model)
{
if (m_rightModel == model)
return;
if (m_rightModel != nullptr || m_rightModelDestroyed) {
qWarning("Changing right model is not supported!");
return;
}
m_rightModel = model;
// see: LeftJoinModel::setLeftModel
connect(m_rightModel, &QAbstractItemModel::rowsInserted,
this, &LeftJoinModel::initializeIfReady);
connect(m_rightModel, &QObject::destroyed, this, [this] {
this->m_rightModel = nullptr;
this->m_rightModelDestroyed = true;
});
emit rightModelChanged();
initializeIfReady();
}
QAbstractItemModel* LeftJoinModel::rightModel() const
{
return m_rightModel;
}
void LeftJoinModel::setJoinRole(const QString& joinRole)
{
if (m_joinRole.isEmpty() && joinRole.isEmpty())
return;
if (!m_joinRole.isEmpty()) {
qWarning("Changing join role is not supported!");
return;
}
m_joinRole = joinRole;
emit joinRoleChanged();
initializeIfReady();
}
const QString& LeftJoinModel::joinRole() const
{
return m_joinRole;
}
void LeftJoinModel::setSourceModel(QAbstractItemModel* newSourceModel)
{
qWarning() << "Source model is not intended to be set directly on this model."
" Use setLeftModel and setRightModel instead!";
}
QHash<int, QByteArray> LeftJoinModel::roleNames() const
{
return m_roleNames;
}

View File

@ -4,6 +4,7 @@
#include <qqmlsortfilterproxymodeltypes.h>
#include "StatusQ/QClipboardProxy.h"
#include "StatusQ/leftjoinmodel.h"
#include "StatusQ/modelutilsinternal.h"
#include "StatusQ/permissionutilsinternal.h"
#include "StatusQ/rolesrenamingmodel.h"
@ -25,6 +26,7 @@ public:
qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter");
qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator");
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");

View File

@ -44,3 +44,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE
add_executable(RolesRenamingModelTest tst_RolesRenamingModel.cpp)
target_link_libraries(RolesRenamingModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
add_test(RolesRenamingModelTest COMMAND RolesRenamingModelTest)
add_executable(LeftJoinModelTest tst_LeftJoinModel.cpp)
target_link_libraries(LeftJoinModelTest PRIVATE Qt5::Test StatusQ)
add_test(LeftJoinModelTest COMMAND LeftJoinModelTest)

View File

@ -0,0 +1,626 @@
#include <QSignalSpy>
#include <QTest>
#include <memory>
#include <StatusQ/leftjoinmodel.h>
namespace {
class TestSourceModel : public QAbstractListModel {
public:
explicit TestSourceModel(QList<QPair<QString, QVariantList>> data)
: m_data(std::move(data))
{
m_roles.reserve(m_data.size());
for (auto i = 0; i < m_data.size(); i++)
m_roles.insert(i, m_data.at(i).first.toUtf8());
}
int rowCount(const QModelIndex& parent) const override
{
assert(m_data.size());
return m_data.first().second.size();
}
QVariant data(const QModelIndex& index, int role) const override
{
if (!index.isValid())
return {};
const auto row = index.row();
if (role >= m_data.length() || row >= m_data.at(0).second.length())
return {};
return m_data.at(role).second.at(row);
}
void insert(int index, QVariantList row)
{
beginInsertRows(QModelIndex{}, index, index);
assert(row.size() == m_data.size());
for (int i = 0; i < m_data.size(); i++) {
auto& roleVariantList = m_data[i].second;
assert(index <= roleVariantList.size());
roleVariantList.insert(index, row.at(i));
}
endInsertRows();
}
void update(int index, int role, QVariant value)
{
assert(role < m_data.size() && index < m_data[role].second.size());
m_data[role].second[index].setValue(std::move(value));
emit dataChanged(this->index(index, 0), this->index(index, 0), { role });
}
void remove(int index)
{
beginRemoveRows(QModelIndex{}, index, index);
for (int i = 0; i < m_data.size(); i++) {
auto& roleVariantList = m_data[i].second;
assert(index < roleVariantList.size());
roleVariantList.removeAt(index);
}
endRemoveRows();
}
QHash<int, QByteArray> roleNames() const override
{
return m_roles;
}
private:
QList<QPair<QString, QVariantList>> m_data;
QHash<int, QByteArray> m_roles;
};
}
class TestLeftJoinModel: public QObject
{
Q_OBJECT
private slots:
void emptyModelTest() {
LeftJoinModel model;
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
}
void initializationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
model.setRightModel(&rightModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
model.setJoinRole("communityId");
QCOMPARE(model.rowCount(), 2);
QHash<int, QByteArray> roles{{0, "title" }, {1, "communityId"}, {2, "name"}};
QCOMPARE(model.roleNames(), roles);
}
void setSourceModelDirectlyTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
QTest::ignoreMessage(QtWarningMsg,
"Source model is not intended to be set directly "
"on this model. Use setLeftModel and setRightModel instead!");
model.setSourceModel(&leftModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
}
void noJoinRoleTest()
{
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
model.setRightModel(&rightModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
QTest::ignoreMessage(QtWarningMsg,
"Both left and right models have to contain "
"join role someRole!");
model.setJoinRole("someRole");
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
}
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
model.setRightModel(&rightModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
QTest::ignoreMessage(QtWarningMsg,
"Both left and right models have to contain "
"join role title!");
model.setJoinRole("title");
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
}
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
model.setRightModel(&rightModel);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
QTest::ignoreMessage(QtWarningMsg,
"Both left and right models have to contain "
"join role name!");
model.setJoinRole("name");
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
}
}
void basicAccesTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(1, 0), 2), QString("Community 2"));
}
void changesPropagationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }},
{ "color", { "red", "green" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
{
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
rightModel.update(0, 0, "Community 1 Updated");
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.first().at(0), model.index(0, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(model.rowCount() - 1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {2});
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1 Updated"));
QCOMPARE(model.data(model.index(1, 0), 2), QString("Community 2"));
}
{
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
leftModel.update(1, 0, "Token 2 Updated");
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.first().at(0), model.index(1, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {0});
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2 Updated"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1 Updated"));
QCOMPARE(model.data(model.index(1, 0), 2), QString("Community 2"));
}
}
void rightModelJoinRoleChangesPropagationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
rightModel.update(1, 1, "community_3");
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.first().at(0), model.index(0, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(model.rowCount() - 1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {2});
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(1, 0), 2), {});
}
void rightModelRemovalPropagationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2" }},
{ "communityId", { "community_1", "community_2" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
rightModel.remove(1);
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.first().at(0), model.index(0, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(model.rowCount() - 1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {2});
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(1, 0), 2), {});
}
void rightModelAdditionPropagationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_3" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
rightModel.insert(2, {"Community 3", "community_3"});
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.first().at(0), model.index(0, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(model.rowCount() - 1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {2});
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(2, 0), 0), QString("Token 3"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_2"));
QCOMPARE(model.data(model.index(2, 0), 1), QString("community_3"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(1, 0), 2), QString("Community 2"));
QCOMPARE(model.data(model.index(2, 0), 2), QString("Community 3"));
}
void leftModelJoinRoleChangesPropagationTest()
{
TestSourceModel leftModel({
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
TestSourceModel rightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(&leftModel);
model.setRightModel(&rightModel);
model.setJoinRole("communityId");
QSignalSpy dataChangedSpy(&model, &LeftJoinModel::dataChanged);
leftModel.update(1, 1, "community_1");
QCOMPARE(dataChangedSpy.count(), 2);
QCOMPARE(dataChangedSpy.first().at(0), model.index(1, 0));
QCOMPARE(dataChangedSpy.first().at(1), model.index(1, 0));
QCOMPARE(dataChangedSpy.first().at(2).value<QVector<int>>(), {2});
QCOMPARE(dataChangedSpy.at(1).at(0), model.index(1, 0));
QCOMPARE(dataChangedSpy.at(1).at(1), model.index(1, 0));
QCOMPARE(dataChangedSpy.at(1).at(2).value<QVector<int>>(), {1});
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0), 0), QString("Token 1"));
QCOMPARE(model.data(model.index(1, 0), 0), QString("Token 2"));
QCOMPARE(model.data(model.index(2, 0), 0), QString("Token 3"));
QCOMPARE(model.data(model.index(0, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(1, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(2, 0), 1), QString("community_1"));
QCOMPARE(model.data(model.index(0, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(1, 0), 2), QString("Community 1"));
QCOMPARE(model.data(model.index(2, 0), 2), QString("Community 1"));
}
void modelsDeletedBeforeInitializationTest()
{
auto leftModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
auto rightModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(leftModel.get());
model.setRightModel(rightModel.get());
leftModel.reset();
rightModel.reset();
model.setJoinRole("communityId");
QCOMPARE(model.roleNames(), {});
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.data(model.index(0, 0), 0), {});
QCOMPARE(model.data(model.index(0, 0), 2), {});
TestSourceModel newLeftModel({
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
TestSourceModel newRightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
QTest::ignoreMessage(QtWarningMsg,
"Changing left model is not supported!");
model.setLeftModel(&newLeftModel);
QTest::ignoreMessage(QtWarningMsg,
"Changing right model is not supported!");
model.setRightModel(&newRightModel);
}
void modelsDeletedAfterInitializationTest()
{
auto leftModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
auto rightModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(leftModel.get());
model.setRightModel(rightModel.get());
model.setJoinRole("communityId");
leftModel.reset();
rightModel.reset();
QHash<int, QByteArray> roles{{0, "title" }, {1, "communityId"}, {2, "name"}};
QCOMPARE(model.roleNames(), roles);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.data(model.index(0, 0), 0), {});
QCOMPARE(model.data(model.index(0, 0), 2), {});
TestSourceModel newLeftModel({
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
TestSourceModel newRightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
QTest::ignoreMessage(QtWarningMsg,
"Changing left model is not supported!");
model.setLeftModel(&newLeftModel);
QTest::ignoreMessage(QtWarningMsg,
"Changing right model is not supported!");
model.setRightModel(&newRightModel);
}
void rightModelDeletedAfterInitializationTest()
{
auto leftModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
auto rightModel = std::make_unique<TestSourceModel>(
QList<QPair<QString, QVariantList>>{
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
LeftJoinModel model;
model.setLeftModel(leftModel.get());
model.setRightModel(rightModel.get());
model.setJoinRole("communityId");
rightModel.reset();
QHash<int, QByteArray> roles{{0, "title" }, {1, "communityId"}, {2, "name"}};
QCOMPARE(model.roleNames(), roles);
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0), 0), "Token 1");
QCOMPARE(model.data(model.index(0, 0), 1), "community_1");
QCOMPARE(model.data(model.index(0, 0), 2), {});
TestSourceModel newLeftModel({
{ "title", { "Token 1", "Token 2", "Token 3"}},
{ "communityId", { "community_1", "community_2", "community_1" }}
});
TestSourceModel newRightModel({
{ "name", { "Community 1", "Community 2" }},
{ "communityId", { "community_1", "community_2" }}
});
QTest::ignoreMessage(QtWarningMsg,
"Changing left model is not supported!");
model.setLeftModel(&newLeftModel);
QTest::ignoreMessage(QtWarningMsg,
"Changing right model is not supported!");
model.setRightModel(&newRightModel);
}
};
QTEST_MAIN(TestLeftJoinModel)
#include "tst_LeftJoinModel.moc"