From e29cd0aebfb471b133be4feb959196f7a4a4cf45 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Sun, 25 Jan 2026 10:47:19 +0100 Subject: [PATCH 01/12] Basic JSON RESTful client fetching items from a local server at application start and adding them to the model. --- CMakeLists.txt | 9 +++-- genericcore.cpp | 44 +++++++++++++++++++++++++ genericcore.h | 13 ++++++++ model/metadata.h | 3 ++ model/tablemodel.cpp | 6 ++++ network/apiroutes.h | 13 ++++++++ network/servercommunicator.cpp | 60 ++++++++++++++++++++++++++++++++++ network/servercommunicator.h | 33 +++++++++++++++++++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 network/apiroutes.h create mode 100644 network/servercommunicator.cpp create mode 100644 network/servercommunicator.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a275e08..da30dcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core LinguistTools) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core LinguistTools Gui) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Network) configure_file(CoreConfig.h.in CoreConfig.h) @@ -32,16 +33,18 @@ add_library(${TARGET_APP} STATIC data/filehandler.h data/filehandler.cpp model/metadata.h formats/csvparser.h formats/csvparser.cpp + model/generalsortfiltermodel.h model/generalsortfiltermodel.cpp + network/servercommunicator.h network/servercommunicator.cpp + network/apiroutes.h # 3rd party libraries ../3rdParty/rapidcsv/src/rapidcsv.h - model/generalsortfiltermodel.h model/generalsortfiltermodel.cpp ) - -target_link_libraries(GenericCore PRIVATE Qt${QT_VERSION_MAJOR}::Test) include_directories(${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(${TARGET_APP} PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui) +target_link_libraries(GenericCore PRIVATE Qt${QT_VERSION_MAJOR}::Test) +target_link_libraries(GenericCore PRIVATE Qt${QT_VERSION_MAJOR}::Network) target_compile_definitions(${TARGET_APP} PRIVATE ${TARGET_APP}_LIBRARY) diff --git a/genericcore.cpp b/genericcore.cpp index 38046a3..c26c2cc 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -16,6 +16,7 @@ #include "model/generalsortfiltermodel.h" #include "model/metadata.h" #include "model/tablemodel.h" +#include "network/servercommunicator.h" #include @@ -37,6 +38,7 @@ GenericCore::GenericCore() { m_modelUndoStack = new QUndoStack(this); setupModels(); + setupServerConfiguration(); } GenericCore::~GenericCore() { qDebug() << "Destroying core..."; } @@ -132,6 +134,32 @@ bool GenericCore::exportCSVFile(const QString& filePath) { return FileHandler::exportToCSVFile(itemsAsStringLists, filePath); } +bool GenericCore::isSyncServerSetup() const { + if (m_serverCommunicator) { + return true; + } else { + return false; + } +} + +void GenericCore::onItemsFetched(const QByteArray jsonDoc) { + emit displayStatusMessage("New items fetched."); + // TODO ? check compability of JSON structure beforehand? + m_mainModel->appendItems(jsonDoc); +} + +void GenericCore::onItemsFetchFailure(const QString errorString) { + emit displayStatusMessage(QString("Error: %1").arg(errorString)); +} + +void GenericCore::onPostRequestSuccessful(const QString message) { + emit displayStatusMessage(message); +} + +void GenericCore::onPostRequestFailure(const QString errorString) { + emit displayStatusMessage(QString("Error: %1").arg(errorString)); +} + void GenericCore::setupModels() { m_mainModel = make_shared(m_modelUndoStack, this); m_sortFilterModel = make_shared(m_mainModel); @@ -188,3 +216,19 @@ QString GenericCore::getMaintenanceToolFilePath() const { const QString filePath = applicationDirPath + "/" + UPDATER_EXE; return filePath; } + +void GenericCore::setupServerConfiguration() { + m_serverCommunicator = make_unique(this); + + connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetched, this, + &GenericCore::onItemsFetched); + connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetchFailure, this, + &GenericCore::onItemsFetchFailure); + connect(m_serverCommunicator.get(), &ServerCommunicator::postRequestSuccessful, this, + &GenericCore::onPostRequestSuccessful); + connect(m_serverCommunicator.get(), &ServerCommunicator::postRequestFailure, this, + &GenericCore::onPostRequestFailure); + + /// fetching items on startup to test the communication with the server + m_serverCommunicator->fetchItems(); +} diff --git a/genericcore.h b/genericcore.h index a7df414..a916647 100644 --- a/genericcore.h +++ b/genericcore.h @@ -10,6 +10,7 @@ class QString; class TableModel; class GeneralSortFilterModel; +class ServerCommunicator; class GenericCore : public QObject { Q_OBJECT @@ -32,6 +33,14 @@ class GenericCore : public QObject { void importCSVFile(const QString& filePath); bool exportCSVFile(const QString& filePath); + bool isSyncServerSetup() const; + + public slots: + void onItemsFetched(const QByteArray jsonDoc); + void onItemsFetchFailure(const QString errorString); + void onPostRequestSuccessful(const QString message); + void onPostRequestFailure(const QString errorString); + signals: void displayStatusMessage(QString message); @@ -46,6 +55,10 @@ class GenericCore : public QObject { void initModelData(); QString getMaintenanceToolFilePath() const; + + /// Network communication + std::unique_ptr m_serverCommunicator; + void setupServerConfiguration(); }; #endif // GENERICCORE_H diff --git a/model/metadata.h b/model/metadata.h index b0bb4b8..91da345 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -6,6 +6,8 @@ #include #include +// TODO add namespace + /// model data enum UserRoles { NameRole = Qt::UserRole + 1, @@ -16,6 +18,7 @@ enum UserRoles { /// helper roles ToStringRole }; + static UserRoles DEFAULT_ROLE = NameRole; static QList USER_FACING_ROLES = {NameRole, DescriptionRole, InfoRole, AmountRole, FactorRole}; diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 08941b6..e22a151 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -193,6 +193,12 @@ void TableModel::insertItems(int startPosition, const QModelIndex& parentIndex) { const QList valueList = JsonParser::toItemValuesList(jsonDoc, ITEM_KEY_STRING); + if (valueList.empty()) { + /// don't try inserting if no values to insert + qDebug() << "No items found in JSON document. Not adding anything..."; + return; + } + insertItems(startPosition, valueList, parentIndex); } diff --git a/network/apiroutes.h b/network/apiroutes.h new file mode 100644 index 0000000..70ef34e --- /dev/null +++ b/network/apiroutes.h @@ -0,0 +1,13 @@ +#ifndef APIROUTES_H +#define APIROUTES_H + +#include + +// TODO add namespace + +static const QString baseUrl = "http://127.0.0.1:4000"; +static const QString apiPrefix = "/api/"; + +static const QString ROUTE_ITEMS = apiPrefix + "items"; + +#endif // APIROUTES_H diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp new file mode 100644 index 0000000..89e07d8 --- /dev/null +++ b/network/servercommunicator.cpp @@ -0,0 +1,60 @@ +#include "servercommunicator.h" +#include "apiroutes.h" + +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +ServerCommunicator::ServerCommunicator(QObject* parent) + : QObject{parent} { + m_netManager.setAutoDeleteReplies(true); + m_restManager = std::make_shared(&m_netManager); + m_serviceApi = std::make_shared(); + setUrl(baseUrl); +} + +bool ServerCommunicator::sslSupported() { +#if QT_CONFIG(ssl) + return QSslSocket::supportsSsl(); +#else + return false; +#endif +} + +QUrl ServerCommunicator::url() const { return m_serviceApi->baseUrl(); } + +void ServerCommunicator::setUrl(const QUrl& url) { + if (m_serviceApi->baseUrl() == url) { + return; + } + m_serviceApi->setBaseUrl(url); + QHttpHeaders authenticationHeaders; + m_serviceApi->setCommonHeaders(authenticationHeaders); + emit urlChanged(); +} + +void ServerCommunicator::fetchItems() { + /// Set up a GET request + m_restManager->get(m_serviceApi->createRequest(ROUTE_ITEMS), this, [this](QRestReply& reply) { + if (reply.isSuccess()) { + qInfo() << "Fetching items successful."; + const QJsonDocument doc = reply.readJson().value(); + emit itemsFetched(doc.toJson()); + + } else { + if (reply.hasError()) { + const QString errorString = reply.errorString(); + qCritical() << "ERROR:" << errorString; + emit itemsFetchFailure(errorString); + } else { + int statusCode = reply.httpStatus(); + qCritical() << "ERROR:" << statusCode; + emit itemsFetchFailure(QString::number(statusCode)); + emit itemsFetchFailure(QString("HTTP status code: %1").arg(statusCode)); + } + } + }); +} diff --git a/network/servercommunicator.h b/network/servercommunicator.h new file mode 100644 index 0000000..5c23469 --- /dev/null +++ b/network/servercommunicator.h @@ -0,0 +1,33 @@ +#ifndef SERVERCOMMUNICATOR_H +#define SERVERCOMMUNICATOR_H + +#include +#include +#include +#include + +class ServerCommunicator : public QObject { + Q_OBJECT + public: + explicit ServerCommunicator(QObject* parent = nullptr); + + bool sslSupported(); + + QUrl url() const; + void setUrl(const QUrl& url); + + void fetchItems(); + + signals: + void urlChanged(); + + void itemsFetched(const QByteArray jsonDoc); + void itemsFetchFailure(const QString errorString); + + private: + QNetworkAccessManager m_netManager; + std::shared_ptr m_restManager; + std::shared_ptr m_serviceApi; +}; + +#endif // SERVERCOMMUNICATOR_H From bc96a805f8321c620fc1d0a80d461e69a3fb4aaa Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Thu, 29 Jan 2026 08:54:47 +0100 Subject: [PATCH 02/12] An item (with hard coded values) can be send to the server. Added signals for fetching and posting items to be triggered from the UI. --- genericcore.cpp | 15 +++++++++++++-- genericcore.h | 3 +++ network/servercommunicator.cpp | 32 ++++++++++++++++++++++++++++++++ network/servercommunicator.h | 4 ++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/genericcore.cpp b/genericcore.cpp index c26c2cc..4394f3b 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -142,6 +142,11 @@ bool GenericCore::isSyncServerSetup() const { } } +void GenericCore::onSendItemTriggered(const int row) { + // NEXT gather item values from model to send to server + m_serverCommunicator->postItems(); +} + void GenericCore::onItemsFetched(const QByteArray jsonDoc) { emit displayStatusMessage("New items fetched."); // TODO ? check compability of JSON structure beforehand? @@ -219,7 +224,12 @@ QString GenericCore::getMaintenanceToolFilePath() const { void GenericCore::setupServerConfiguration() { m_serverCommunicator = make_unique(this); + /// request connections + connect(this, &GenericCore::fetchItemsFromServer, m_serverCommunicator.get(), + &ServerCommunicator::fetchItems); + connect(this, &GenericCore::sendItemToServer, this, &GenericCore::onSendItemTriggered); + /// response connections connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetched, this, &GenericCore::onItemsFetched); connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetchFailure, this, @@ -229,6 +239,7 @@ void GenericCore::setupServerConfiguration() { connect(m_serverCommunicator.get(), &ServerCommunicator::postRequestFailure, this, &GenericCore::onPostRequestFailure); - /// fetching items on startup to test the communication with the server - m_serverCommunicator->fetchItems(); + /// fetching/posting items on startup to test the communication with the server + // m_serverCommunicator->fetchItems(); + // m_serverCommunicator->postItems(); } diff --git a/genericcore.h b/genericcore.h index a916647..8e4e8b3 100644 --- a/genericcore.h +++ b/genericcore.h @@ -36,6 +36,7 @@ class GenericCore : public QObject { bool isSyncServerSetup() const; public slots: + void onSendItemTriggered(const int row); void onItemsFetched(const QByteArray jsonDoc); void onItemsFetchFailure(const QString errorString); void onPostRequestSuccessful(const QString message); @@ -43,6 +44,8 @@ class GenericCore : public QObject { signals: void displayStatusMessage(QString message); + void fetchItemsFromServer(); + void sendItemToServer(int row); private: QUndoStack* m_modelUndoStack; diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp index 89e07d8..c4fce47 100644 --- a/network/servercommunicator.cpp +++ b/network/servercommunicator.cpp @@ -32,6 +32,7 @@ void ServerCommunicator::setUrl(const QUrl& url) { } m_serviceApi->setBaseUrl(url); QHttpHeaders authenticationHeaders; + authenticationHeaders.append(QHttpHeaders::WellKnownHeader::ContentType, "application/json"); m_serviceApi->setCommonHeaders(authenticationHeaders); emit urlChanged(); } @@ -58,3 +59,34 @@ void ServerCommunicator::fetchItems() { } }); } + +void ServerCommunicator::postItems() { + QJsonObject itemObject; + itemObject["name"] = "Post test 1"; + itemObject["description"] = "Post description 1"; + itemObject["info"] = "Post info 1"; + itemObject["amount"] = 1; + itemObject["factor"] = 5.3; + + QJsonObject rootObject; + rootObject.insert("item", itemObject); + + QJsonDocument jsonDoc(rootObject); + QByteArray jsonData = jsonDoc.toJson(); + + QNetworkReply* reply = m_restManager->post(m_serviceApi->createRequest(ROUTE_ITEMS), jsonData); + + QObject::connect(reply, &QNetworkReply::finished, [=]() { + if (reply->error() == QNetworkReply::NoError) { + QByteArray responseData = reply->readAll(); + const QString message = QString("POST successful! Response: %1").arg(responseData); + qInfo() << message; + emit postRequestSuccessful(message); + } else { + const QString message = QString("Error: %1").arg(reply->errorString()); + qDebug() << message; + emit postRequestFailure(message); + } + reply->deleteLater(); + }); +} diff --git a/network/servercommunicator.h b/network/servercommunicator.h index 5c23469..73ac5f4 100644 --- a/network/servercommunicator.h +++ b/network/servercommunicator.h @@ -16,13 +16,17 @@ class ServerCommunicator : public QObject { QUrl url() const; void setUrl(const QUrl& url); + public slots: void fetchItems(); + void postItems(); /// NEXT add item(s) as argument; signals: void urlChanged(); void itemsFetched(const QByteArray jsonDoc); void itemsFetchFailure(const QString errorString); + void postRequestSuccessful(const QString message); + void postRequestFailure(const QString errorString); private: QNetworkAccessManager m_netManager; From 08c2e3a0937b5e57fb95454a0ef655bf84de6ce6 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Thu, 29 Jan 2026 09:27:50 +0100 Subject: [PATCH 03/12] Added IdRole for the server generated UUID of the items. --- formats/jsonparser.cpp | 8 ++++++++ genericcore.cpp | 2 ++ model/metadata.h | 21 +++++++++++---------- model/modelitem.cpp | 13 +++++++++++++ model/tablemodel.cpp | 8 ++++---- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/formats/jsonparser.cpp b/formats/jsonparser.cpp index aa24722..a29f8e7 100644 --- a/formats/jsonparser.cpp +++ b/formats/jsonparser.cpp @@ -75,6 +75,14 @@ QJsonArray JsonParser::extractItemArray(const QByteArray& jsonData, const QStrin ModelItemValues JsonParser::jsonObjectToItemValues(const QJsonObject& itemJsonObject) { ModelItemValues values; + const UserRoles idRole = IdRole; + const QString idRoleName = ROLE_NAMES.value(idRole); + // QVariant idValue = data(idRole); + if (itemJsonObject.contains(idRoleName)) { + std::pair keyValuePair = getKeyValuePair(itemJsonObject, idRole); + values.insert(keyValuePair.first, keyValuePair.second); + } + QListIterator i(USER_FACING_ROLES); while (i.hasNext()) { const UserRoles role = i.next(); diff --git a/genericcore.cpp b/genericcore.cpp index 4394f3b..e757136 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -150,6 +150,7 @@ void GenericCore::onSendItemTriggered(const int row) { void GenericCore::onItemsFetched(const QByteArray jsonDoc) { emit displayStatusMessage("New items fetched."); // TODO ? check compability of JSON structure beforehand? + // TODO check if item already exists? m_mainModel->appendItems(jsonDoc); } @@ -158,6 +159,7 @@ void GenericCore::onItemsFetchFailure(const QString errorString) { } void GenericCore::onPostRequestSuccessful(const QString message) { + // NEXT search local item an set server generated UUID emit displayStatusMessage(message); } diff --git a/model/metadata.h b/model/metadata.h index 91da345..b5c4bda 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -16,20 +16,21 @@ enum UserRoles { AmountRole, FactorRole, /// helper roles - ToStringRole + ToStringRole, + IdRole }; -static UserRoles DEFAULT_ROLE = NameRole; +static UserRoles DEFAULT_ROLE = NameRole; +// TODO ?rename USER_FACING_ROLES -> MAIN_ROLES ? static QList USER_FACING_ROLES = {NameRole, DescriptionRole, InfoRole, AmountRole, FactorRole}; -static QHash ROLE_NAMES = {{NameRole, "Name"}, - {DescriptionRole, "Description"}, - {InfoRole, "Info"}, - {AmountRole, "Amount"}, - {FactorRole, "Factor"}}; -static QList STRING_ROLES = {NameRole, DescriptionRole, InfoRole}; -static QList INT_ROLES = {AmountRole}; -static QList DOUBLE_ROLES = {FactorRole}; +static QHash ROLE_NAMES = { + {NameRole, "Name"}, {DescriptionRole, "Description"}, {InfoRole, "Info"}, + {AmountRole, "Amount"}, {FactorRole, "Factor"}, {ToStringRole, "ToString"}, + {IdRole, "id"}}; +static QList STRING_ROLES = {NameRole, DescriptionRole, InfoRole, IdRole}; +static QList INT_ROLES = {AmountRole}; +static QList DOUBLE_ROLES = {FactorRole}; /// JSON keys static const QString ITEM_KEY_STRING = "items"; diff --git a/model/modelitem.cpp b/model/modelitem.cpp index a096705..88ac31b 100644 --- a/model/modelitem.cpp +++ b/model/modelitem.cpp @@ -55,12 +55,25 @@ QString ModelItem::toString() const { // result.append(value.toString()); result.append(QString("%1: %2\n").arg(roleName, data(role).toString())); } + + const UserRoles idRole = IdRole; + QVariant idValue = data(idRole); + if (!idValue.isNull()) { + const QString idRoleName = ROLE_NAMES.value(idRole); + result.append(QString("%1: %2\n").arg(idRoleName, idValue.toString())); + } return result; } QJsonObject ModelItem::toJsonObject() const { QJsonObject itemObject; // TODO add UUID and dates (entry, modification, end) + const UserRoles idRole = IdRole; + QVariant idValue = data(idRole); + if (!idValue.isNull()) { + const QString idRoleName = ROLE_NAMES.value(idRole); + itemObject.insert(idRoleName, idValue.toString()); + } QListIterator i(USER_FACING_ROLES); while (i.hasNext()) { diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index e22a151..504ceec 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -49,16 +49,16 @@ QHash TableModel::roleNames() const { return ROLE_NAMES; } int TableModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { - return 0; // no children + return 0; /// no children } return m_items.size(); } int TableModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) { - return 0; // no children + return 0; /// no children } - return ROLE_NAMES.size(); + return USER_FACING_ROLES.size(); } QVariant TableModel::data(const QModelIndex& index, int role) const { @@ -249,7 +249,7 @@ void TableModel::execEditItemData(const int row, const QMap& chan /// is getting notified about (potential) data changes; dataChanged should be called only for /// the affected columns const QModelIndex firstIndex = this->index(row, 0); - const QModelIndex lastIndex = this->index(row, ROLE_NAMES.size() - 1); + const QModelIndex lastIndex = this->index(row, USER_FACING_ROLES.size() - 1); QList roles = changedValues.keys(); roles.insert(0, Qt::DisplayRole); emit dataChanged(firstIndex, lastIndex, roles.toVector()); From 2a152daa7018b28026c5745a25793748b197c91f Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Thu, 29 Jan 2026 13:06:46 +0100 Subject: [PATCH 04/12] Using lower case role names for improved compatibility with the server. --- model/metadata.h | 4 ++-- model/tablemodel.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/model/metadata.h b/model/metadata.h index b5c4bda..20a5e06 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -25,8 +25,8 @@ static UserRoles DEFAULT_ROLE = NameRole; static QList USER_FACING_ROLES = {NameRole, DescriptionRole, InfoRole, AmountRole, FactorRole}; static QHash ROLE_NAMES = { - {NameRole, "Name"}, {DescriptionRole, "Description"}, {InfoRole, "Info"}, - {AmountRole, "Amount"}, {FactorRole, "Factor"}, {ToStringRole, "ToString"}, + {NameRole, "name"}, {DescriptionRole, "description"}, {InfoRole, "info"}, + {AmountRole, "amount"}, {FactorRole, "factor"}, {ToStringRole, "ToString"}, {IdRole, "id"}}; static QList STRING_ROLES = {NameRole, DescriptionRole, InfoRole, IdRole}; static QList INT_ROLES = {AmountRole}; diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 504ceec..f11e3b3 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -16,6 +16,7 @@ QByteArray TableModel::generateExampleItems() { QJsonObject rootObject; QJsonArray array; + // TODO use JsonParser for the item generation for (int row = 0; row < 5; ++row) { QJsonObject itemObject; // itemObject.insert("uuid", m_uuid.toString()); From bedf8084d30465dd685da738f0cdce83da83ac15 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Thu, 29 Jan 2026 13:13:32 +0100 Subject: [PATCH 05/12] Retrieving the JSON data of the current index to send to the server through the proxy model. Sending this data happens by triggering the core. --- genericcore.cpp | 9 ++++----- genericcore.h | 6 +++--- model/generalsortfiltermodel.cpp | 5 +++++ model/generalsortfiltermodel.h | 2 ++ model/metadata.h | 6 ++++-- model/tablemodel.cpp | 11 +++++++++++ model/tablemodel.h | 2 ++ network/servercommunicator.cpp | 15 +-------------- network/servercommunicator.h | 3 ++- 9 files changed, 34 insertions(+), 25 deletions(-) diff --git a/genericcore.cpp b/genericcore.cpp index e757136..707037f 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -142,16 +142,15 @@ bool GenericCore::isSyncServerSetup() const { } } -void GenericCore::onSendItemTriggered(const int row) { - // NEXT gather item values from model to send to server - m_serverCommunicator->postItems(); +void GenericCore::onSendItemTriggered(const QByteArray& jsonData) { + m_serverCommunicator->postItems(jsonData); } -void GenericCore::onItemsFetched(const QByteArray jsonDoc) { +void GenericCore::onItemsFetched(const QByteArray jsonData) { emit displayStatusMessage("New items fetched."); // TODO ? check compability of JSON structure beforehand? // TODO check if item already exists? - m_mainModel->appendItems(jsonDoc); + m_mainModel->appendItems(jsonData); } void GenericCore::onItemsFetchFailure(const QString errorString) { diff --git a/genericcore.h b/genericcore.h index 8e4e8b3..9fc1e2a 100644 --- a/genericcore.h +++ b/genericcore.h @@ -36,8 +36,8 @@ class GenericCore : public QObject { bool isSyncServerSetup() const; public slots: - void onSendItemTriggered(const int row); - void onItemsFetched(const QByteArray jsonDoc); + void onSendItemTriggered(const QByteArray& jsonData); + void onItemsFetched(const QByteArray jsonData); void onItemsFetchFailure(const QString errorString); void onPostRequestSuccessful(const QString message); void onPostRequestFailure(const QString errorString); @@ -45,7 +45,7 @@ class GenericCore : public QObject { signals: void displayStatusMessage(QString message); void fetchItemsFromServer(); - void sendItemToServer(int row); + void sendItemToServer(const QByteArray& jsonData); private: QUndoStack* m_modelUndoStack; diff --git a/model/generalsortfiltermodel.cpp b/model/generalsortfiltermodel.cpp index 0b1133e..75784e5 100644 --- a/model/generalsortfiltermodel.cpp +++ b/model/generalsortfiltermodel.cpp @@ -23,6 +23,11 @@ QItemSelection GeneralSortFilterModel::findItems(const QString& text) const { return result; } +QByteArray GeneralSortFilterModel::jsonDataForServer(const QModelIndex& proxyIndex) { + const QModelIndex sourceIndex = mapToSource(proxyIndex); + return m_tableModel->jsonDataForServer(sourceIndex); +} + void GeneralSortFilterModel::appendItems(const QByteArray& jsonDoc) { m_tableModel->appendItems(jsonDoc); } diff --git a/model/generalsortfiltermodel.h b/model/generalsortfiltermodel.h index 3fa4a8b..9afdabc 100644 --- a/model/generalsortfiltermodel.h +++ b/model/generalsortfiltermodel.h @@ -18,6 +18,8 @@ class GeneralSortFilterModel : public QSortFilterProxyModel { */ QItemSelection findItems(const QString& text) const; + QByteArray jsonDataForServer(const QModelIndex& proxyIndex); + public slots: void appendItems(const QByteArray& jsonDoc); bool removeRows(int firstRow, int nRows, const QModelIndex& parentIndex = QModelIndex()) override; diff --git a/model/metadata.h b/model/metadata.h index 20a5e06..ce42c60 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -15,9 +15,11 @@ enum UserRoles { InfoRole, AmountRole, FactorRole, - /// helper roles + /// Non user facing + IdRole, + /// read only roles ToStringRole, - IdRole + ToJsonRole }; static UserRoles DEFAULT_ROLE = NameRole; diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index f11e3b3..6d97e03 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -86,6 +86,8 @@ QVariant TableModel::data(const QModelIndex& index, int role) const { return m_items.at(row)->data(role); case ToStringRole: return m_items.at(row)->toString(); + case ToJsonRole: + return m_items.at(row)->toJsonObject(); } return QVariant(); @@ -169,6 +171,15 @@ QList TableModel::getItemsAsStringLists() const { return result; } +// TODO use item selection as parameter to wrap multiple items into JSON data structure +QByteArray TableModel::jsonDataForServer(const QModelIndex& currentIndex) const { + const QJsonObject itemObject = data(currentIndex, ToJsonRole).toJsonObject(); + QJsonObject rootObject; + rootObject.insert("item", itemObject); + const QJsonDocument jsonDoc(rootObject); + return jsonDoc.toJson(QJsonDocument::Compact); +} + bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIndex) { if (parentIndex != QModelIndex()) { qWarning() << "Removing of child rows is not supported yet!"; diff --git a/model/tablemodel.h b/model/tablemodel.h index 4bd2dcd..4017d55 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -38,6 +38,8 @@ class TableModel : public QAbstractTableModel { QJsonDocument getAllItemsAsJsonDoc() const; QList getItemsAsStringLists() const; + QByteArray jsonDataForServer(const QModelIndex& currentIndex) const; + public slots: // bool insertRows(int position, int rows, const QModelIndex& parentIndex = QModelIndex()) // override; diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp index c4fce47..feb41ea 100644 --- a/network/servercommunicator.cpp +++ b/network/servercommunicator.cpp @@ -60,20 +60,7 @@ void ServerCommunicator::fetchItems() { }); } -void ServerCommunicator::postItems() { - QJsonObject itemObject; - itemObject["name"] = "Post test 1"; - itemObject["description"] = "Post description 1"; - itemObject["info"] = "Post info 1"; - itemObject["amount"] = 1; - itemObject["factor"] = 5.3; - - QJsonObject rootObject; - rootObject.insert("item", itemObject); - - QJsonDocument jsonDoc(rootObject); - QByteArray jsonData = jsonDoc.toJson(); - +void ServerCommunicator::postItems(const QByteArray& jsonData) { QNetworkReply* reply = m_restManager->post(m_serviceApi->createRequest(ROUTE_ITEMS), jsonData); QObject::connect(reply, &QNetworkReply::finished, [=]() { diff --git a/network/servercommunicator.h b/network/servercommunicator.h index 73ac5f4..257a4c8 100644 --- a/network/servercommunicator.h +++ b/network/servercommunicator.h @@ -18,7 +18,8 @@ class ServerCommunicator : public QObject { public slots: void fetchItems(); - void postItems(); /// NEXT add item(s) as argument; + void postItems(const QByteArray& jsonData); + // NEXT void deleteItems(QList idList) signals: void urlChanged(); From 63fe96fb2e02865ace0db64cdf73859cd6aaa54f Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Mon, 2 Feb 2026 16:13:44 +0100 Subject: [PATCH 06/12] On sending (posting) item to the server the generated UUID is added to the local item. --- data/filehandler.h | 2 +- formats/csvparser.cpp | 4 +- formats/csvparser.h | 2 +- formats/jsonparser.cpp | 25 ++++++-- formats/jsonparser.h | 4 +- genericcore.cpp | 16 +++--- genericcore.h | 4 +- model/commands/edititemcommand.cpp | 5 ++ model/commands/insertrowscommand.h | 2 +- model/commands/removerowscommand.h | 2 +- model/metadata.h | 5 +- model/modelitem.h | 4 +- model/tablemodel.cpp | 92 ++++++++++++++++++++++++++++-- model/tablemodel.h | 7 ++- network/servercommunicator.cpp | 2 +- network/servercommunicator.h | 2 +- 16 files changed, 145 insertions(+), 33 deletions(-) diff --git a/data/filehandler.h b/data/filehandler.h index 6fd6019..c9bb097 100644 --- a/data/filehandler.h +++ b/data/filehandler.h @@ -3,7 +3,7 @@ #include -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; class QJsonDocument; class QString; diff --git a/formats/csvparser.cpp b/formats/csvparser.cpp index 365de1d..85e5bac 100644 --- a/formats/csvparser.cpp +++ b/formats/csvparser.cpp @@ -86,7 +86,7 @@ QHash> CsvParser::extractColumnValues( const rapidcsv::Document& doc) { QHash> columnValueMap; for (const QString& columnName : headerNames) { - // NEXT add support for optional columns + // TODO add support for optional columns // if (optionalCsvHeaderNames.contains(columnName)) { // const std::vector columnNames = doc.GetColumnNames(); // int columnIdx = doc.GetColumnIdx(columnName.toStdString()); @@ -147,7 +147,7 @@ QVariant CsvParser::parseItemValue(const int role, const std::string& valueStrin result = doubleValue; // } else if (typeColumns.contains(columnName)) { - // // NEXT validate string is allowed + // // TODO validate string is allowed // values[role] = QString::fromStdString(columnValueMap.value(columnName).at(row)); } else { /// no type recognized for column diff --git a/formats/csvparser.h b/formats/csvparser.h index 7695156..b1375d9 100644 --- a/formats/csvparser.h +++ b/formats/csvparser.h @@ -3,7 +3,7 @@ #include -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; namespace rapidcsv { class Document; diff --git a/formats/jsonparser.cpp b/formats/jsonparser.cpp index a29f8e7..686f4bc 100644 --- a/formats/jsonparser.cpp +++ b/formats/jsonparser.cpp @@ -6,20 +6,35 @@ #include "../model/metadata.h" QList JsonParser::toItemValuesList(const QByteArray& jsonData, - const QString& objectName) { + const QString& rootValueName) { QList result; if (jsonData.isEmpty()) { return result; } - QJsonArray itemArray = extractItemArray(jsonData, objectName); + // NEXT tidy up the following code and encapsulate into functions; - foreach (QJsonValue value, itemArray) { - QJsonObject itemJsonObject = value.toObject(); + /// check if there is an array or there is only one object inside the root object; + // TODO ? rename objectName into jsonValueName? + if (rootValueName == ITEMS_KEY_STRING) { + QJsonArray itemArray = extractItemArray(jsonData, rootValueName); + + foreach (QJsonValue value, itemArray) { + QJsonObject itemJsonObject = value.toObject(); + ModelItemValues values = jsonObjectToItemValues(itemJsonObject); + result.append(values); + } + } + if (rootValueName == ITEM_KEY_STRING) { + // QJsonArray itemArray = extractItemArray(jsonData, objectName); + QJsonDocument doc = QJsonDocument::fromJson(jsonData); + QJsonObject rootObject = doc.object(); + QJsonObject itemJsonObject = rootObject.value(rootValueName).toObject(); ModelItemValues values = jsonObjectToItemValues(itemJsonObject); result.append(values); } + return result; } @@ -28,7 +43,7 @@ QByteArray JsonParser::itemValuesListToJson(const QList& itemVa QJsonDocument jsonDoc; QJsonObject rootObject; QJsonArray itemArray; - for (const QHash& itemValues : itemValuesList) { + for (const ModelItemValues& itemValues : itemValuesList) { QJsonObject itemObject; QListIterator i(USER_FACING_ROLES); diff --git a/formats/jsonparser.h b/formats/jsonparser.h index be0fc6a..a27d223 100644 --- a/formats/jsonparser.h +++ b/formats/jsonparser.h @@ -8,14 +8,14 @@ class QString; class QByteArray; class QJsonArray; -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; using namespace std; class JsonParser { public: static QList toItemValuesList(const QByteArray& jsonData, - const QString& objectName = ""); + const QString& rootValueName = ""); static QByteArray itemValuesListToJson(const QList& itemValuesList, const QString& objectName = ""); diff --git a/genericcore.cpp b/genericcore.cpp index 707037f..539e6ab 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -105,7 +105,7 @@ void GenericCore::saveItems() { qDebug() << "saving items..."; const QJsonDocument doc = m_mainModel->getAllItemsAsJsonDoc(); - const bool successfulSave = FileHandler::saveToFile(doc, ITEM_FILE_NAME); + const bool successfulSave = FileHandler::saveToFile(doc, ITEMS_FILE_NAME); if (successfulSave) { m_modelUndoStack->setClean(); emit displayStatusMessage(QString("Items saved.")); @@ -118,7 +118,7 @@ void GenericCore::importCSVFile(const QString& filePath) { qInfo() << "importing items from CSV..."; qDebug() << "filePath:" << filePath; const QList itemValuesList = FileHandler::getItemValuesFromCSVFile(filePath); - // NEXT inform UI on errors + // TODO inform UI on errors if (itemValuesList.isEmpty()) { qDebug() << "No items found. Doing nothing..."; return; @@ -149,7 +149,7 @@ void GenericCore::onSendItemTriggered(const QByteArray& jsonData) { void GenericCore::onItemsFetched(const QByteArray jsonData) { emit displayStatusMessage("New items fetched."); // TODO ? check compability of JSON structure beforehand? - // TODO check if item already exists? + // NEXT check if item already exists ? ; m_mainModel->appendItems(jsonData); } @@ -157,8 +157,10 @@ void GenericCore::onItemsFetchFailure(const QString errorString) { emit displayStatusMessage(QString("Error: %1").arg(errorString)); } -void GenericCore::onPostRequestSuccessful(const QString message) { - // NEXT search local item an set server generated UUID +void GenericCore::onPostRequestSuccessful(const QByteArray responseData) { + // NEXT search local item and set server generated UUID; + const QString message = m_mainModel->updateItemsFromJson(responseData); + emit displayStatusMessage(message); } @@ -193,7 +195,7 @@ void GenericCore::setupModels() { */ void GenericCore::initModelData() { qInfo() << "Trying to read model data from file..."; - const QByteArray jsonDoc = FileHandler::loadJSONDataFromFile(ITEM_FILE_NAME); + const QByteArray jsonDoc = FileHandler::loadJSONDataFromFile(ITEMS_FILE_NAME); // qDebug() << "jsonDoc:" << jsonDoc; // TODO decide on lack of file(s) (config, data) if example items should be generated // (see welcome wizard) @@ -228,7 +230,7 @@ void GenericCore::setupServerConfiguration() { /// request connections connect(this, &GenericCore::fetchItemsFromServer, m_serverCommunicator.get(), &ServerCommunicator::fetchItems); - connect(this, &GenericCore::sendItemToServer, this, &GenericCore::onSendItemTriggered); + connect(this, &GenericCore::postItemToServer, this, &GenericCore::onSendItemTriggered); /// response connections connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetched, this, diff --git a/genericcore.h b/genericcore.h index 9fc1e2a..66877f9 100644 --- a/genericcore.h +++ b/genericcore.h @@ -39,13 +39,13 @@ class GenericCore : public QObject { void onSendItemTriggered(const QByteArray& jsonData); void onItemsFetched(const QByteArray jsonData); void onItemsFetchFailure(const QString errorString); - void onPostRequestSuccessful(const QString message); + void onPostRequestSuccessful(const QByteArray responseData); void onPostRequestFailure(const QString errorString); signals: void displayStatusMessage(QString message); void fetchItemsFromServer(); - void sendItemToServer(const QByteArray& jsonData); + void postItemToServer(const QByteArray& jsonData); private: QUndoStack* m_modelUndoStack; diff --git a/model/commands/edititemcommand.cpp b/model/commands/edititemcommand.cpp index 449272b..f9280f8 100644 --- a/model/commands/edititemcommand.cpp +++ b/model/commands/edititemcommand.cpp @@ -26,6 +26,11 @@ EditItemCommand::EditItemCommand(TableModel* model, .arg(roleName) .arg(index.data(DEFAULT_ROLE).toString()) .arg(value.toString()); + } else if (role == IdRole) { + commandText = QString("Setting '%1' of item '%2' to '%3'") + .arg(roleName) + .arg(index.data(DEFAULT_ROLE).toString()) + .arg(value.toString()); } else { qWarning() << "Role didn't match! Using a generic command text..."; commandText = QString("Edit item '%1'").arg(index.data(DEFAULT_ROLE).toString()); diff --git a/model/commands/insertrowscommand.h b/model/commands/insertrowscommand.h index ddc5617..64dff89 100644 --- a/model/commands/insertrowscommand.h +++ b/model/commands/insertrowscommand.h @@ -3,7 +3,7 @@ #include -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; class TableModel; diff --git a/model/commands/removerowscommand.h b/model/commands/removerowscommand.h index 6703aab..21b314e 100644 --- a/model/commands/removerowscommand.h +++ b/model/commands/removerowscommand.h @@ -5,7 +5,7 @@ class TableModel; -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; class RemoveRowsCommand : public QUndoCommand { public: diff --git a/model/metadata.h b/model/metadata.h index ce42c60..eadb1d0 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -35,10 +35,11 @@ static QList INT_ROLES = {AmountRole}; static QList DOUBLE_ROLES = {FactorRole}; /// JSON keys -static const QString ITEM_KEY_STRING = "items"; +static const QString ITEMS_KEY_STRING = "items"; +static const QString ITEM_KEY_STRING = "item"; /// file naming -static const QString ITEM_FILE_NAME = ITEM_KEY_STRING + ".json"; +static const QString ITEMS_FILE_NAME = ITEMS_KEY_STRING + ".json"; /// functions static int GET_ROLE_FOR_COLUMN(const int column) { diff --git a/model/modelitem.h b/model/modelitem.h index 5d2e956..1019296 100644 --- a/model/modelitem.h +++ b/model/modelitem.h @@ -3,7 +3,7 @@ #include -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; class ModelItem { public: @@ -18,7 +18,7 @@ class ModelItem { QJsonObject toJsonObject() const; private: - QHash m_values; + ModelItemValues m_values; }; #endif // MODELITEM_H diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 6d97e03..0b3460f 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -29,7 +29,7 @@ QByteArray TableModel::generateExampleItems() { array.append(itemObject); } - rootObject.insert(ITEM_KEY_STRING, array); + rootObject.insert(ITEMS_KEY_STRING, array); doc.setObject(rootObject); return doc.toJson(); @@ -83,6 +83,7 @@ QVariant TableModel::data(const QModelIndex& index, int role) const { case InfoRole: case AmountRole: case FactorRole: + case IdRole: return m_items.at(row)->data(role); case ToStringRole: return m_items.at(row)->toString(); @@ -152,7 +153,7 @@ QJsonDocument TableModel::getAllItemsAsJsonDoc() const { QJsonObject itemObject = item->toJsonObject(); array.append(itemObject); } - rootObject.insert(ITEM_KEY_STRING, array); + rootObject.insert(ITEMS_KEY_STRING, array); doc.setObject(rootObject); return doc; @@ -175,11 +176,37 @@ QList TableModel::getItemsAsStringLists() const { QByteArray TableModel::jsonDataForServer(const QModelIndex& currentIndex) const { const QJsonObject itemObject = data(currentIndex, ToJsonRole).toJsonObject(); QJsonObject rootObject; - rootObject.insert("item", itemObject); + rootObject.insert(ITEM_KEY_STRING, itemObject); const QJsonDocument jsonDoc(rootObject); return jsonDoc.toJson(QJsonDocument::Compact); } +QString TableModel::updateItemsFromJson(const QByteArray& jsonData) { + /// convert JSON data into a list of item values + const QList valueList = JsonParser::toItemValuesList(jsonData, ITEM_KEY_STRING); + /// for each item values: + // NEXT iterate over all value items in the list (or disable updating multiple items for now) + // for (ModelItemValues itemValues : valueList) { + // } + // NEXT encapsulate into updateItem(const ModelItemValues& itemValues) + QModelIndex foundIndex = searchItemIndex(valueList.first()); + + qDebug() << "Search done!"; + if (foundIndex == QModelIndex()) { + const QString errorMessage = "No matching item found!"; + qWarning() << errorMessage; + return errorMessage; + } else { + qInfo() << "Item found!"; + /// update existing item + QMap roles = valueList.first(); + setItemData(foundIndex, roles); + /// return status what happened in this function (i. e. "Created x items, updated y items.") + // return data(foundIndex, ToStringRole).toString(); + return QString("Item found at row %1.").arg(foundIndex.row()); + } +} + bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIndex) { if (parentIndex != QModelIndex()) { qWarning() << "Removing of child rows is not supported yet!"; @@ -203,7 +230,7 @@ void TableModel::appendItems(const QByteArray& jsonDoc) { insertItems(-1, jsonDo void TableModel::insertItems(int startPosition, const QByteArray& jsonDoc, const QModelIndex& parentIndex) { - const QList valueList = JsonParser::toItemValuesList(jsonDoc, ITEM_KEY_STRING); + const QList valueList = JsonParser::toItemValuesList(jsonDoc, ITEMS_KEY_STRING); if (valueList.empty()) { /// don't try inserting if no values to insert @@ -302,3 +329,60 @@ bool TableModel::isEmptyValueEqualToZero(const int role) const { return false; } } + +QModelIndex TableModel::searchItemIndex(const ModelItemValues givenItemValues) const { + // iterate over indexes to search item : see searchItem(...); + // for (const shared_ptr& item : m_items) { + for (int row = 0; row < rowCount(); ++row) { + qDebug() << "Processing item at row" << row << "..."; + QModelIndex itemIndex = index(row, 0); + if (isItemEqualToItemValues(itemIndex, givenItemValues)) { + qInfo() << "Found item at row" << row << "! Returning its index..."; + return itemIndex; + } + } + qDebug() << "No matching item found. Returning empty pointer..."; + return {}; +} + +bool TableModel::isItemEqualToItemValues(const QModelIndex& itemIndex, + const ModelItemValues givenItemValues) const { + /// do both have a UUID? + QVariant idOfItem = data(itemIndex, IdRole); + QVariant given = givenItemValues.value(IdRole); + if (idOfItem.isValid() && given.isValid()) { + /// are the UUIDs the same? + if (idOfItem.toString() == given.toString()) { + qInfo() << "UUIDs are the same."; + return true; + } else { + qDebug() << "UUIDs are NOT the same."; + return false; + } + } else { + /// are all other values the same? (for now only USER_FACING_ROLES are checked) + QListIterator i(USER_FACING_ROLES); + while (i.hasNext()) { + const UserRoles role = i.next(); + const QString roleName = ROLE_NAMES.value(role); + const QVariant valueOfItem = data(itemIndex, role); + const QVariant givenValue = givenItemValues.value(role); + if (STRING_ROLES.contains(role)) { + if (valueOfItem.toString() != givenValue.toString()) { + return false; + } + } else if (INT_ROLES.contains(role)) { + if (valueOfItem.toInt() != givenValue.toInt()) { + return false; + } + } else if (DOUBLE_ROLES.contains(role)) { + if (valueOfItem.toDouble() != givenValue.toDouble()) { + return false; + } + } else { + qCritical() << QString("Can't find data type for role %1!!!").arg(role); + } + } + return true; + } +} diff --git a/model/tablemodel.h b/model/tablemodel.h index 4017d55..84c592f 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -8,7 +8,7 @@ class ModelItem; using namespace std; -typedef QHash ModelItemValues; +typedef QMap ModelItemValues; class TableModel : public QAbstractTableModel { Q_OBJECT @@ -40,6 +40,8 @@ class TableModel : public QAbstractTableModel { QByteArray jsonDataForServer(const QModelIndex& currentIndex) const; + QString updateItemsFromJson(const QByteArray& jsonData); + public slots: // bool insertRows(int position, int rows, const QModelIndex& parentIndex = QModelIndex()) // override; @@ -68,6 +70,9 @@ class TableModel : public QAbstractTableModel { QMap onlyChangedValues(const QModelIndex& index, const QMap& roleValueMap) const; bool isEmptyValueEqualToZero(const int role) const; + QModelIndex searchItemIndex(const ModelItemValues givenItemValues) const; + bool isItemEqualToItemValues(const QModelIndex& itemIndex, + const ModelItemValues givenItemValues) const; }; #endif // TABLEMODEL_H diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp index feb41ea..a48dae1 100644 --- a/network/servercommunicator.cpp +++ b/network/servercommunicator.cpp @@ -68,7 +68,7 @@ void ServerCommunicator::postItems(const QByteArray& jsonData) { QByteArray responseData = reply->readAll(); const QString message = QString("POST successful! Response: %1").arg(responseData); qInfo() << message; - emit postRequestSuccessful(message); + emit postRequestSuccessful(responseData); } else { const QString message = QString("Error: %1").arg(reply->errorString()); qDebug() << message; diff --git a/network/servercommunicator.h b/network/servercommunicator.h index 257a4c8..6fdb7ba 100644 --- a/network/servercommunicator.h +++ b/network/servercommunicator.h @@ -26,7 +26,7 @@ class ServerCommunicator : public QObject { void itemsFetched(const QByteArray jsonDoc); void itemsFetchFailure(const QString errorString); - void postRequestSuccessful(const QString message); + void postRequestSuccessful(const QByteArray responseData); void postRequestFailure(const QString errorString); private: From db1ecbece0c98ddcbc1efb75fd2b6f3d2708d336 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Mon, 2 Feb 2026 16:15:20 +0100 Subject: [PATCH 07/12] Current items can be deleted by passing the UUID to the deleteItemFromServer signal. --- genericcore.cpp | 14 ++++++++++++++ genericcore.h | 3 +++ model/tablemodel.cpp | 1 - network/servercommunicator.cpp | 19 +++++++++++++++++++ network/servercommunicator.h | 4 +++- 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/genericcore.cpp b/genericcore.cpp index 539e6ab..1327f67 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -168,6 +168,14 @@ void GenericCore::onPostRequestFailure(const QString errorString) { emit displayStatusMessage(QString("Error: %1").arg(errorString)); } +void GenericCore::onDeleteRequestSuccessful(const QByteArray responseData) { + qWarning() << "TODO: Process success response!!!"; +} + +void GenericCore::onDeleteRequestFailure(const QString errorString) { + qWarning() << "TODO: Process error response!!!"; +} + void GenericCore::setupModels() { m_mainModel = make_shared(m_modelUndoStack, this); m_sortFilterModel = make_shared(m_mainModel); @@ -231,6 +239,8 @@ void GenericCore::setupServerConfiguration() { connect(this, &GenericCore::fetchItemsFromServer, m_serverCommunicator.get(), &ServerCommunicator::fetchItems); connect(this, &GenericCore::postItemToServer, this, &GenericCore::onSendItemTriggered); + connect(this, &GenericCore::deleteItemFromServer, m_serverCommunicator.get(), + &ServerCommunicator::deleteItem); /// response connections connect(m_serverCommunicator.get(), &ServerCommunicator::itemsFetched, this, @@ -241,6 +251,10 @@ void GenericCore::setupServerConfiguration() { &GenericCore::onPostRequestSuccessful); connect(m_serverCommunicator.get(), &ServerCommunicator::postRequestFailure, this, &GenericCore::onPostRequestFailure); + connect(m_serverCommunicator.get(), &ServerCommunicator::deleteRequestSuccessful, this, + &GenericCore::onDeleteRequestSuccessful); + connect(m_serverCommunicator.get(), &ServerCommunicator::deleteRequestFailure, this, + &GenericCore::onDeleteRequestFailure); /// fetching/posting items on startup to test the communication with the server // m_serverCommunicator->fetchItems(); diff --git a/genericcore.h b/genericcore.h index 66877f9..f939692 100644 --- a/genericcore.h +++ b/genericcore.h @@ -41,11 +41,14 @@ class GenericCore : public QObject { void onItemsFetchFailure(const QString errorString); void onPostRequestSuccessful(const QByteArray responseData); void onPostRequestFailure(const QString errorString); + void onDeleteRequestSuccessful(const QByteArray responseData); + void onDeleteRequestFailure(const QString errorString); signals: void displayStatusMessage(QString message); void fetchItemsFromServer(); void postItemToServer(const QByteArray& jsonData); + void deleteItemFromServer(const QString& id); private: QUndoStack* m_modelUndoStack; diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 0b3460f..c78bd70 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -202,7 +202,6 @@ QString TableModel::updateItemsFromJson(const QByteArray& jsonData) { QMap roles = valueList.first(); setItemData(foundIndex, roles); /// return status what happened in this function (i. e. "Created x items, updated y items.") - // return data(foundIndex, ToStringRole).toString(); return QString("Item found at row %1.").arg(foundIndex.row()); } } diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp index a48dae1..cab273e 100644 --- a/network/servercommunicator.cpp +++ b/network/servercommunicator.cpp @@ -77,3 +77,22 @@ void ServerCommunicator::postItems(const QByteArray& jsonData) { reply->deleteLater(); }); } + +void ServerCommunicator::deleteItem(const QString& id) { + const QString deleteRoute = QString("%1/%2").arg(ROUTE_ITEMS, id); + QNetworkReply* reply = m_restManager->deleteResource(m_serviceApi->createRequest(deleteRoute)); + + QObject::connect(reply, &QNetworkReply::finished, [=]() { + if (reply->error() == QNetworkReply::NoError) { + QByteArray responseData = reply->readAll(); + const QString message = QString("DELETE successful! Response: %1").arg(responseData); + qInfo() << message; + emit deleteRequestSuccessful(responseData); + } else { + const QString message = QString("Error: %1").arg(reply->errorString()); + qDebug() << message; + emit deleteRequestFailure(message); + } + reply->deleteLater(); + }); +} diff --git a/network/servercommunicator.h b/network/servercommunicator.h index 6fdb7ba..e364b6d 100644 --- a/network/servercommunicator.h +++ b/network/servercommunicator.h @@ -19,7 +19,7 @@ class ServerCommunicator : public QObject { public slots: void fetchItems(); void postItems(const QByteArray& jsonData); - // NEXT void deleteItems(QList idList) + void deleteItem(const QString& id); signals: void urlChanged(); @@ -28,6 +28,8 @@ class ServerCommunicator : public QObject { void itemsFetchFailure(const QString errorString); void postRequestSuccessful(const QByteArray responseData); void postRequestFailure(const QString errorString); + void deleteRequestSuccessful(const QByteArray responseData); + void deleteRequestFailure(const QString errorString); private: QNetworkAccessManager m_netManager; From ba482e6e17c4274b4590b00accc650f6b871dbdf Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Mon, 2 Feb 2026 16:46:10 +0100 Subject: [PATCH 08/12] Added getUuid(itemIndex) to proxy model to prevent using IdRole in UI when an item should be referenced (i. e. for deletion on server). --- model/generalsortfiltermodel.cpp | 4 ++++ model/generalsortfiltermodel.h | 1 + 2 files changed, 5 insertions(+) diff --git a/model/generalsortfiltermodel.cpp b/model/generalsortfiltermodel.cpp index 75784e5..1dd5cf3 100644 --- a/model/generalsortfiltermodel.cpp +++ b/model/generalsortfiltermodel.cpp @@ -23,6 +23,10 @@ QItemSelection GeneralSortFilterModel::findItems(const QString& text) const { return result; } +QString GeneralSortFilterModel::getUuid(const QModelIndex& itemIndex) const { + return data(itemIndex, IdRole).toString(); +} + QByteArray GeneralSortFilterModel::jsonDataForServer(const QModelIndex& proxyIndex) { const QModelIndex sourceIndex = mapToSource(proxyIndex); return m_tableModel->jsonDataForServer(sourceIndex); diff --git a/model/generalsortfiltermodel.h b/model/generalsortfiltermodel.h index 9afdabc..a5eade5 100644 --- a/model/generalsortfiltermodel.h +++ b/model/generalsortfiltermodel.h @@ -17,6 +17,7 @@ class GeneralSortFilterModel : public QSortFilterProxyModel { * @return QItemSelection containing all successful ModelIndex results */ QItemSelection findItems(const QString& text) const; + QString getUuid(const QModelIndex& itemIndex) const; QByteArray jsonDataForServer(const QModelIndex& proxyIndex); From d4ff1ffb614568f596f24cfe7cb3d03c675c34d0 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Mon, 2 Feb 2026 16:48:42 +0100 Subject: [PATCH 09/12] Split TableModel::updateItemsFromJson(...) into two functions and added support for multiple items in JSON data. --- genericcore.cpp | 2 -- model/tablemodel.cpp | 27 +++++++++++++++------------ model/tablemodel.h | 1 + 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/genericcore.cpp b/genericcore.cpp index 1327f67..44875c5 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -158,9 +158,7 @@ void GenericCore::onItemsFetchFailure(const QString errorString) { } void GenericCore::onPostRequestSuccessful(const QByteArray responseData) { - // NEXT search local item and set server generated UUID; const QString message = m_mainModel->updateItemsFromJson(responseData); - emit displayStatusMessage(message); } diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index c78bd70..76e87a7 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -182,27 +182,30 @@ QByteArray TableModel::jsonDataForServer(const QModelIndex& currentIndex) const } QString TableModel::updateItemsFromJson(const QByteArray& jsonData) { - /// convert JSON data into a list of item values const QList valueList = JsonParser::toItemValuesList(jsonData, ITEM_KEY_STRING); - /// for each item values: - // NEXT iterate over all value items in the list (or disable updating multiple items for now) - // for (ModelItemValues itemValues : valueList) { - // } - // NEXT encapsulate into updateItem(const ModelItemValues& itemValues) - QModelIndex foundIndex = searchItemIndex(valueList.first()); + int nChangedItems = 0; + for (const ModelItemValues& itemValues : valueList) { + bool updateHappened = updateItem(itemValues); + if (updateHappened) { + nChangedItems++; + } + } + return QString("Found and updated %1 item(s).").arg(nChangedItems); +} + +bool TableModel::updateItem(const ModelItemValues& itemValues) { + QModelIndex foundIndex = searchItemIndex(itemValues); qDebug() << "Search done!"; if (foundIndex == QModelIndex()) { const QString errorMessage = "No matching item found!"; qWarning() << errorMessage; - return errorMessage; + return false; } else { qInfo() << "Item found!"; /// update existing item - QMap roles = valueList.first(); - setItemData(foundIndex, roles); - /// return status what happened in this function (i. e. "Created x items, updated y items.") - return QString("Item found at row %1.").arg(foundIndex.row()); + setItemData(foundIndex, itemValues); + return true; } } diff --git a/model/tablemodel.h b/model/tablemodel.h index 84c592f..a7d2089 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -41,6 +41,7 @@ class TableModel : public QAbstractTableModel { QByteArray jsonDataForServer(const QModelIndex& currentIndex) const; QString updateItemsFromJson(const QByteArray& jsonData); + bool updateItem(const ModelItemValues& itemValues); public slots: // bool insertRows(int position, int rows, const QModelIndex& parentIndex = QModelIndex()) From 6adf18caebd03ad0f80c3c6ccc10011f677f1bd3 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Tue, 3 Feb 2026 11:17:26 +0100 Subject: [PATCH 10/12] Server settings are read from QSettings and applied if they are changed. --- genericcore.cpp | 23 ++++++++++++++++++++--- genericcore.h | 2 ++ network/apiroutes.h | 1 - network/servercommunicator.cpp | 10 +++++++++- network/servercommunicator.h | 6 ++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/genericcore.cpp b/genericcore.cpp index 44875c5..8a527f2 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -13,6 +13,7 @@ #include "CoreConfig.h" #include "constants.h" #include "data/filehandler.h" +#include "data/settingshandler.h" #include "model/generalsortfiltermodel.h" #include "model/metadata.h" #include "model/tablemodel.h" @@ -134,6 +135,14 @@ bool GenericCore::exportCSVFile(const QString& filePath) { return FileHandler::exportToCSVFile(itemsAsStringLists, filePath); } +void GenericCore::applySettings(QVariantMap settingMap, QString group) { + SettingsHandler::saveSettings(settingMap, group); + + if (group == "Server") { + setupServerConfiguration(); + } +} + bool GenericCore::isSyncServerSetup() const { if (m_serverCommunicator) { return true; @@ -254,7 +263,15 @@ void GenericCore::setupServerConfiguration() { connect(m_serverCommunicator.get(), &ServerCommunicator::deleteRequestFailure, this, &GenericCore::onDeleteRequestFailure); - /// fetching/posting items on startup to test the communication with the server - // m_serverCommunicator->fetchItems(); - // m_serverCommunicator->postItems(); + applyServerConfiguration(); +} + +void GenericCore::applyServerConfiguration() { + const QVariantMap serverSettings = SettingsHandler::getSettings("Server"); + const QString urlValue = serverSettings.value("url").toString(); + if (!urlValue.isEmpty()) { + const QString emailValue = serverSettings.value("email").toString(); + const QString passwordValue = serverSettings.value("password").toString(); + m_serverCommunicator->setServerConfiguration(urlValue, emailValue, passwordValue); + } } diff --git a/genericcore.h b/genericcore.h index f939692..1c39ef0 100644 --- a/genericcore.h +++ b/genericcore.h @@ -33,6 +33,7 @@ class GenericCore : public QObject { void importCSVFile(const QString& filePath); bool exportCSVFile(const QString& filePath); + void applySettings(QVariantMap settingMap, QString group = ""); bool isSyncServerSetup() const; public slots: @@ -65,6 +66,7 @@ class GenericCore : public QObject { /// Network communication std::unique_ptr m_serverCommunicator; void setupServerConfiguration(); + void applyServerConfiguration(); }; #endif // GENERICCORE_H diff --git a/network/apiroutes.h b/network/apiroutes.h index 70ef34e..e89da3a 100644 --- a/network/apiroutes.h +++ b/network/apiroutes.h @@ -5,7 +5,6 @@ // TODO add namespace -static const QString baseUrl = "http://127.0.0.1:4000"; static const QString apiPrefix = "/api/"; static const QString ROUTE_ITEMS = apiPrefix + "items"; diff --git a/network/servercommunicator.cpp b/network/servercommunicator.cpp index cab273e..7545e2e 100644 --- a/network/servercommunicator.cpp +++ b/network/servercommunicator.cpp @@ -13,7 +13,6 @@ ServerCommunicator::ServerCommunicator(QObject* parent) m_netManager.setAutoDeleteReplies(true); m_restManager = std::make_shared(&m_netManager); m_serviceApi = std::make_shared(); - setUrl(baseUrl); } bool ServerCommunicator::sslSupported() { @@ -96,3 +95,12 @@ void ServerCommunicator::deleteItem(const QString& id) { reply->deleteLater(); }); } + +void ServerCommunicator::setServerConfiguration(const QString url, + const QString email, + const QString password) { + setUrl(url); + + m_email = email; + m_password = password; +} diff --git a/network/servercommunicator.h b/network/servercommunicator.h index e364b6d..3107604 100644 --- a/network/servercommunicator.h +++ b/network/servercommunicator.h @@ -16,6 +16,8 @@ class ServerCommunicator : public QObject { QUrl url() const; void setUrl(const QUrl& url); + void setServerConfiguration(const QString url, const QString email, const QString password); + public slots: void fetchItems(); void postItems(const QByteArray& jsonData); @@ -35,6 +37,10 @@ class ServerCommunicator : public QObject { QNetworkAccessManager m_netManager; std::shared_ptr m_restManager; std::shared_ptr m_serviceApi; + + QString m_email; + QString m_password; + // QString m_authToken; }; #endif // SERVERCOMMUNICATOR_H From 7ae10e6ed72c81032737a58e7dd43b3ba2e14a47 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Tue, 3 Feb 2026 11:19:24 +0100 Subject: [PATCH 11/12] Added GenericCore::getSettings(...) for the UIs (they don't need to use the NetworkHandler directly). --- genericcore.cpp | 4 ++++ genericcore.h | 1 + 2 files changed, 5 insertions(+) diff --git a/genericcore.cpp b/genericcore.cpp index 8a527f2..1d0eaba 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -135,6 +135,10 @@ bool GenericCore::exportCSVFile(const QString& filePath) { return FileHandler::exportToCSVFile(itemsAsStringLists, filePath); } +QVariantMap GenericCore::getSettings(QString group) const { + return SettingsHandler::getSettings(group); +} + void GenericCore::applySettings(QVariantMap settingMap, QString group) { SettingsHandler::saveSettings(settingMap, group); diff --git a/genericcore.h b/genericcore.h index 1c39ef0..4bf30ea 100644 --- a/genericcore.h +++ b/genericcore.h @@ -33,6 +33,7 @@ class GenericCore : public QObject { void importCSVFile(const QString& filePath); bool exportCSVFile(const QString& filePath); + QVariantMap getSettings(QString group = "") const; void applySettings(QVariantMap settingMap, QString group = ""); bool isSyncServerSetup() const; From b6c49dda204f3844293356033e7ad46fc7411a3f Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Tue, 3 Feb 2026 11:55:43 +0100 Subject: [PATCH 12/12] Minor clean up in JsonParser. --- formats/jsonparser.cpp | 16 ++++++---------- formats/jsonparser.h | 2 +- genericcore.cpp | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/formats/jsonparser.cpp b/formats/jsonparser.cpp index 686f4bc..8ef90de 100644 --- a/formats/jsonparser.cpp +++ b/formats/jsonparser.cpp @@ -12,13 +12,13 @@ QList JsonParser::toItemValuesList(const QByteArray& jsonData, if (jsonData.isEmpty()) { return result; } + // TODO tidy up the following code and encapsulate into functions; - // NEXT tidy up the following code and encapsulate into functions; + QJsonDocument doc = QJsonDocument::fromJson(jsonData); - /// check if there is an array or there is only one object inside the root object; - // TODO ? rename objectName into jsonValueName? + /// case one: json value name in plural -> should contain an array of items if (rootValueName == ITEMS_KEY_STRING) { - QJsonArray itemArray = extractItemArray(jsonData, rootValueName); + QJsonArray itemArray = extractItemArray(doc, rootValueName); foreach (QJsonValue value, itemArray) { QJsonObject itemJsonObject = value.toObject(); @@ -26,9 +26,8 @@ QList JsonParser::toItemValuesList(const QByteArray& jsonData, result.append(values); } } + /// case two: json value name in singular -> should contain an object of one item if (rootValueName == ITEM_KEY_STRING) { - // QJsonArray itemArray = extractItemArray(jsonData, objectName); - QJsonDocument doc = QJsonDocument::fromJson(jsonData); QJsonObject rootObject = doc.object(); QJsonObject itemJsonObject = rootObject.value(rootValueName).toObject(); ModelItemValues values = jsonObjectToItemValues(itemJsonObject); @@ -73,17 +72,14 @@ QByteArray JsonParser::itemValuesListToJson(const QList& itemVa JsonParser::JsonParser() {} -QJsonArray JsonParser::extractItemArray(const QByteArray& jsonData, const QString& objectName) { - QJsonDocument doc = QJsonDocument::fromJson(jsonData); +QJsonArray JsonParser::extractItemArray(const QJsonDocument& doc, const QString& objectName) { QJsonArray itemArray; if (objectName.isEmpty()) { itemArray = doc.array(); - } else { QJsonObject rootObject = doc.object(); itemArray = rootObject.value(objectName).toArray(); } - return itemArray; } diff --git a/formats/jsonparser.h b/formats/jsonparser.h index a27d223..bb920fd 100644 --- a/formats/jsonparser.h +++ b/formats/jsonparser.h @@ -22,7 +22,7 @@ class JsonParser { private: explicit JsonParser(); - static QJsonArray extractItemArray(const QByteArray& jsonData, const QString& objectName); + static QJsonArray extractItemArray(const QJsonDocument& doc, const QString& objectName); static ModelItemValues jsonObjectToItemValues(const QJsonObject& itemJsonObject); static pair getKeyValuePair(const QJsonObject& itemJsonObject, const int role); diff --git a/genericcore.cpp b/genericcore.cpp index 1d0eaba..dcba36f 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -162,7 +162,7 @@ void GenericCore::onSendItemTriggered(const QByteArray& jsonData) { void GenericCore::onItemsFetched(const QByteArray jsonData) { emit displayStatusMessage("New items fetched."); // TODO ? check compability of JSON structure beforehand? - // NEXT check if item already exists ? ; + // NEXT check if item already exists and apply changes (UUID,...) ? ; m_mainModel->appendItems(jsonData); }