feat(StatusQ): General purpose js function-based aggregator
Closes: #14617
This commit is contained in:
parent
ae636ef5a7
commit
587594f3b9
|
@ -0,0 +1,155 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import Storybook 1.0
|
||||
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
font.pixelSize: 16
|
||||
padding: 10
|
||||
|
||||
ListModel {
|
||||
id: sourceModel
|
||||
|
||||
ListElement {
|
||||
symbol: "SNT"
|
||||
balance: "4"
|
||||
}
|
||||
ListElement {
|
||||
symbol: "ETH"
|
||||
balance: "14"
|
||||
}
|
||||
ListElement {
|
||||
symbol: "ZRX"
|
||||
balance: "24"
|
||||
}
|
||||
ListElement {
|
||||
symbol: "DAI"
|
||||
balance: "43"
|
||||
}
|
||||
ListElement {
|
||||
symbol: "UNI"
|
||||
balance: "2"
|
||||
}
|
||||
ListElement {
|
||||
symbol: "PEPE"
|
||||
balance: "1"
|
||||
}
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: totalBalanceAggregator
|
||||
|
||||
model: sourceModel
|
||||
initialValue: "0"
|
||||
roleName: "balance"
|
||||
|
||||
aggregateFunction: (aggr, value) => AmountsArithmetic.sum(
|
||||
AmountsArithmetic.fromString(aggr),
|
||||
AmountsArithmetic.fromString(value)).toString()
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: maxBalanceAggregator
|
||||
|
||||
model: sourceModel
|
||||
initialValue: "0"
|
||||
roleName: "balance"
|
||||
|
||||
aggregateFunction: (aggr, value) => AmountsArithmetic.cmp(
|
||||
AmountsArithmetic.fromString(aggr),
|
||||
AmountsArithmetic.fromString(value)) > 0
|
||||
? aggr : value
|
||||
}
|
||||
|
||||
FunctionAggregator {
|
||||
id: tokensListAggregator
|
||||
|
||||
model: sourceModel
|
||||
initialValue: []
|
||||
roleName: "symbol"
|
||||
|
||||
aggregateFunction: (aggr, value) => [...aggr, value]
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
Label {
|
||||
text: "SUMMARY"
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Total balance: " + totalBalanceAggregator.value
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Max balance: " + maxBalanceAggregator.value
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Tokens list: " + tokensListAggregator.value
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "MODEL (click rows to change)"
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
GenericListView {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: sourceModel
|
||||
|
||||
onRowClicked: {
|
||||
if (role === "balance") {
|
||||
const balance = sourceModel.get(index).balance
|
||||
|
||||
sourceModel.setProperty(index, "balance",
|
||||
(Number(balance) + 1).toString())
|
||||
} else {
|
||||
const symbol = sourceModel.get(index).symbol
|
||||
|
||||
sourceModel.setProperty(index, "symbol", symbol + "_")
|
||||
}
|
||||
}
|
||||
|
||||
insetComponent: Button {
|
||||
height: 20
|
||||
font.pixelSize: 11
|
||||
text: "remove"
|
||||
|
||||
onClicked: {
|
||||
sourceModel.remove(model.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Add token"
|
||||
|
||||
property int counter: 1
|
||||
|
||||
onClicked: {
|
||||
sourceModel.append({
|
||||
symbol: "NEW_" + counter,
|
||||
balance: "" + counter * 2
|
||||
})
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Models
|
|
@ -37,6 +37,7 @@ ListView {
|
|||
bottomMargin: margin
|
||||
|
||||
signal moveRequested(int from, int to)
|
||||
signal rowClicked(int index, string role)
|
||||
|
||||
ListModel {
|
||||
id: rowModel
|
||||
|
@ -154,6 +155,14 @@ ListView {
|
|||
readonly property string separator: last ? "" : ","
|
||||
|
||||
text: `${roleName}: ${valueSanitized}${separator}`
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: root.rowClicked(
|
||||
delegateRoot.topModel.index,
|
||||
roleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/fastexpressionrole.h
|
||||
include/StatusQ/fastexpressionsorter.h
|
||||
include/StatusQ/formatteddoubleproperty.h
|
||||
include/StatusQ/functionaggregator.h
|
||||
include/StatusQ/leftjoinmodel.h
|
||||
include/StatusQ/modelutilsinternal.h
|
||||
include/StatusQ/movablemodel.h
|
||||
|
@ -120,6 +121,7 @@ add_library(StatusQ SHARED
|
|||
src/fastexpressionrole.cpp
|
||||
src/fastexpressionsorter.cpp
|
||||
src/formatteddoubleproperty.cpp
|
||||
src/functionaggregator.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
src/movablemodel.cpp
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <QJSValue>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
#include "StatusQ/singleroleaggregator.h"
|
||||
|
||||
class FunctionAggregator : public SingleRoleAggregator
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QVariant initialValue READ initialValue WRITE setInitialValue
|
||||
NOTIFY initialValueChanged)
|
||||
|
||||
Q_PROPERTY(QJSValue aggregateFunction READ aggregateFunction
|
||||
WRITE setAggregateFunction NOTIFY aggregateFunctionChanged)
|
||||
|
||||
public:
|
||||
explicit FunctionAggregator(QObject* parent = nullptr);
|
||||
|
||||
const QVariant& initialValue() const;
|
||||
void setInitialValue(const QVariant& initialValue);
|
||||
|
||||
const QJSValue& aggregateFunction() const;
|
||||
void setAggregateFunction(const QJSValue& aggregateFunction);
|
||||
|
||||
signals:
|
||||
void initialValueChanged();
|
||||
void aggregateFunctionChanged();
|
||||
|
||||
protected slots:
|
||||
QVariant calculateAggregation() override;
|
||||
|
||||
private:
|
||||
QVariant m_initialValue;
|
||||
QJSValue m_aggregateFunction;
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
#include "StatusQ/functionaggregator.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJSEngine>
|
||||
|
||||
FunctionAggregator::FunctionAggregator(QObject* parent)
|
||||
: SingleRoleAggregator(parent)
|
||||
{
|
||||
recalculate();
|
||||
}
|
||||
|
||||
const QVariant& FunctionAggregator::initialValue() const
|
||||
{
|
||||
return m_initialValue;
|
||||
}
|
||||
|
||||
void FunctionAggregator::setInitialValue(const QVariant& initialValue)
|
||||
{
|
||||
if (m_initialValue == initialValue)
|
||||
return;
|
||||
|
||||
m_initialValue = initialValue;
|
||||
|
||||
emit initialValueChanged();
|
||||
recalculate();
|
||||
}
|
||||
|
||||
const QJSValue& FunctionAggregator::aggregateFunction() const
|
||||
{
|
||||
return m_aggregateFunction;
|
||||
}
|
||||
|
||||
void FunctionAggregator::setAggregateFunction(const QJSValue& aggregateFunction)
|
||||
{
|
||||
if (m_aggregateFunction.strictlyEquals(aggregateFunction))
|
||||
return;
|
||||
|
||||
if (!aggregateFunction.isCallable() && !aggregateFunction.isUndefined()) {
|
||||
qWarning() << "FunctionAggregator::aggregateFunction must be a "
|
||||
"callable object.";
|
||||
return;
|
||||
}
|
||||
|
||||
m_aggregateFunction = aggregateFunction;
|
||||
|
||||
emit aggregateFunctionChanged();
|
||||
recalculate();
|
||||
}
|
||||
|
||||
QVariant FunctionAggregator::calculateAggregation()
|
||||
{
|
||||
// Check if m_model exists and role name is initialized
|
||||
if (!model() || roleName().isEmpty())
|
||||
return m_initialValue;
|
||||
|
||||
// Check if m_roleName is part of the roles of the model
|
||||
QHash<int, QByteArray> roles = model()->roleNames();
|
||||
if (!roleExists() && model()->rowCount()) {
|
||||
qWarning() << "Provided role name does not exist in the current model.";
|
||||
return m_initialValue;
|
||||
}
|
||||
|
||||
if (!m_initialValue.isValid())
|
||||
return m_initialValue;
|
||||
|
||||
if (m_aggregateFunction.isUndefined())
|
||||
return m_initialValue;
|
||||
|
||||
QJSEngine* engine = qjsEngine(this);
|
||||
|
||||
if (engine == nullptr) {
|
||||
qWarning() << "FunctionAggregator is intended to be used in JS "
|
||||
"environment. QJSEngine must be available.";
|
||||
return m_initialValue;
|
||||
}
|
||||
|
||||
QJSValue aggregation = engine->toScriptValue(m_initialValue);
|
||||
|
||||
auto rows = model()->rowCount();
|
||||
auto role = roles.key(roleName());
|
||||
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
QModelIndex index = model()->index(i, 0);
|
||||
QVariant value = model()->data(index, role);
|
||||
|
||||
QJSValue valueJs = engine->toScriptValue(value);
|
||||
|
||||
aggregation = m_aggregateFunction.call({aggregation, valueJs});
|
||||
|
||||
if (aggregation.isError()) {
|
||||
qWarning() << "Aggregation calculation failed. Error type:"
|
||||
<< aggregation.errorType();
|
||||
return m_initialValue;
|
||||
}
|
||||
}
|
||||
|
||||
return aggregation.toVariant();
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
#include "StatusQ/fastexpressionrole.h"
|
||||
#include "StatusQ/fastexpressionsorter.h"
|
||||
#include "StatusQ/formatteddoubleproperty.h"
|
||||
#include "StatusQ/functionaggregator.h"
|
||||
#include "StatusQ/leftjoinmodel.h"
|
||||
#include "StatusQ/modelutilsinternal.h"
|
||||
#include "StatusQ/movablemodel.h"
|
||||
|
@ -56,6 +57,7 @@ public:
|
|||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
|
||||
qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator");
|
||||
qmlRegisterType<FunctionAggregator>("StatusQ", 0, 1, "FunctionAggregator");
|
||||
qmlRegisterType<WritableProxyModel>("StatusQ", 0, 1, "WritableProxyModel");
|
||||
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
|
||||
|
||||
|
|
|
@ -82,6 +82,10 @@ add_executable(SumAggregatorTest tst_SumAggregator.cpp)
|
|||
target_link_libraries(SumAggregatorTest PRIVATE StatusQ StatusQTestLib)
|
||||
add_test(NAME SumAggregatorTest COMMAND SumAggregatorTest)
|
||||
|
||||
add_executable(FunctionAggregatorTest tst_FunctionAggregator.cpp)
|
||||
target_link_libraries(FunctionAggregatorTest PRIVATE StatusQ StatusQTestLib)
|
||||
add_test(NAME FunctionAggregatorTest COMMAND FunctionAggregatorTest)
|
||||
|
||||
add_executable(ConcatModelTest tst_ConcatModel.cpp)
|
||||
target_link_libraries(ConcatModelTest PRIVATE StatusQ StatusQTestLib SortFilterProxyModel)
|
||||
add_test(NAME ConcatModelTest COMMAND ConcatModelTest)
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
#include <QtTest>
|
||||
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <StatusQ/functionaggregator.h>
|
||||
#include <TestHelpers/testmodel.h>
|
||||
|
||||
class TestFunctionAggregator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
void makeQmlEngineAvailable(QQmlEngine& engine, QObject& obj)
|
||||
{
|
||||
auto jsObj = engine.newQObject(&obj);
|
||||
engine.setObjectOwnership(&obj, QQmlEngine::CppOwnership);
|
||||
Q_UNUSED(jsObj);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void basicTest() {
|
||||
QQmlEngine engine;
|
||||
FunctionAggregator aggregator;
|
||||
makeQmlEngineAvailable(engine, aggregator);
|
||||
|
||||
auto jsLambda = engine.evaluate("(aggr, val) => [...aggr, val]");
|
||||
QCOMPARE(jsLambda.isError(), false);
|
||||
QCOMPARE(jsLambda.isCallable(), true);
|
||||
|
||||
TestModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "4", "3", "5", "5" }}
|
||||
});
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
aggregator.setRoleName("balance");
|
||||
aggregator.setInitialValue(QVariantList());
|
||||
aggregator.setAggregateFunction(jsLambda);
|
||||
|
||||
QVariantList expected{"4", "3", "5", "5"};
|
||||
QCOMPARE(aggregator.value(), expected);
|
||||
}
|
||||
|
||||
void typeMismatchTest() {
|
||||
QQmlEngine engine;
|
||||
FunctionAggregator aggregator;
|
||||
makeQmlEngineAvailable(engine, aggregator);
|
||||
|
||||
auto jsLambda = engine.evaluate("(aggr, val) => [...aggr, val]");
|
||||
QCOMPARE(jsLambda.isError(), false);
|
||||
QCOMPARE(jsLambda.isCallable(), true);
|
||||
|
||||
TestModel sourceModel({{ "balance", { "4", "3", "5", "5" }}});
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
aggregator.setRoleName("balance");
|
||||
aggregator.setInitialValue(0);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"Aggregation calculation failed. Error type: 6");
|
||||
|
||||
aggregator.setAggregateFunction(jsLambda);
|
||||
|
||||
QCOMPARE(aggregator.value(), 0);
|
||||
}
|
||||
|
||||
void roleNameNotFoundTest() {
|
||||
QQmlEngine engine;
|
||||
FunctionAggregator aggregator;
|
||||
makeQmlEngineAvailable(engine, aggregator);
|
||||
|
||||
TestModel sourceModel({{ "balance", { "4", "3", "5", "5" }}});
|
||||
aggregator.setModel(&sourceModel);
|
||||
aggregator.setInitialValue(0);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"Provided role name does not exist in the current model.");
|
||||
|
||||
aggregator.setRoleName("notExisiting");
|
||||
QCOMPARE(aggregator.value(), 0);
|
||||
}
|
||||
|
||||
void invalidFunctionTest() {
|
||||
FunctionAggregator aggregator;
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"FunctionAggregator::aggregateFunction must be a "
|
||||
"callable object.");
|
||||
aggregator.setAggregateFunction(5);
|
||||
}
|
||||
|
||||
void noJsEngineTest() {
|
||||
QQmlEngine engine;
|
||||
FunctionAggregator aggregator;
|
||||
|
||||
auto jsLambda = engine.evaluate("(aggr) => aggr");
|
||||
QCOMPARE(jsLambda.isError(), false);
|
||||
QCOMPARE(jsLambda.isCallable(), true);
|
||||
|
||||
TestModel sourceModel({
|
||||
{ "balance", { "4", "3", "5", "5" }}
|
||||
});
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
aggregator.setRoleName("balance");
|
||||
aggregator.setInitialValue(0);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"FunctionAggregator is intended to be used in JS "
|
||||
"environment. QJSEngine must be available.");
|
||||
|
||||
aggregator.setAggregateFunction(jsLambda);
|
||||
QCOMPARE(aggregator.value(), 0);
|
||||
}
|
||||
|
||||
void providingInitialValueIfNotReadyTest() {
|
||||
QQmlEngine engine;
|
||||
FunctionAggregator aggregator;
|
||||
makeQmlEngineAvailable(engine, aggregator);
|
||||
|
||||
auto jsLambda = engine.evaluate("(aggr, val) => aggr + val");
|
||||
QCOMPARE(jsLambda.isError(), false);
|
||||
QCOMPARE(jsLambda.isCallable(), true);
|
||||
|
||||
TestModel sourceModel({
|
||||
{ "chainId", { "12", "13", "1", "321" }},
|
||||
{ "balance", { "4", "3", "5", "5" }}
|
||||
});
|
||||
|
||||
QCOMPARE(aggregator.value(), {});
|
||||
|
||||
aggregator.setInitialValue("-");
|
||||
QCOMPARE(aggregator.value(), "-");
|
||||
|
||||
aggregator.setModel(&sourceModel);
|
||||
QCOMPARE(aggregator.value(), "-");
|
||||
|
||||
aggregator.setRoleName("balance");
|
||||
QCOMPARE(aggregator.value(), "-");
|
||||
|
||||
aggregator.setAggregateFunction(jsLambda);
|
||||
QCOMPARE(aggregator.value(), "-4355");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestFunctionAggregator)
|
||||
#include "tst_FunctionAggregator.moc"
|
Loading…
Reference in New Issue