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/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 aa24722..8ef90de 100644 --- a/formats/jsonparser.cpp +++ b/formats/jsonparser.cpp @@ -6,20 +6,34 @@ #include "../model/metadata.h" QList JsonParser::toItemValuesList(const QByteArray& jsonData, - const QString& objectName) { + const QString& rootValueName) { QList result; if (jsonData.isEmpty()) { return result; } + // TODO tidy up the following code and encapsulate into functions; - QJsonArray itemArray = extractItemArray(jsonData, objectName); + QJsonDocument doc = QJsonDocument::fromJson(jsonData); - foreach (QJsonValue value, itemArray) { - QJsonObject itemJsonObject = value.toObject(); + /// case one: json value name in plural -> should contain an array of items + if (rootValueName == ITEMS_KEY_STRING) { + QJsonArray itemArray = extractItemArray(doc, rootValueName); + + foreach (QJsonValue value, itemArray) { + QJsonObject itemJsonObject = value.toObject(); + ModelItemValues values = jsonObjectToItemValues(itemJsonObject); + result.append(values); + } + } + /// case two: json value name in singular -> should contain an object of one item + if (rootValueName == ITEM_KEY_STRING) { + QJsonObject rootObject = doc.object(); + QJsonObject itemJsonObject = rootObject.value(rootValueName).toObject(); ModelItemValues values = jsonObjectToItemValues(itemJsonObject); result.append(values); } + return result; } @@ -28,7 +42,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); @@ -58,23 +72,28 @@ 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; } 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/formats/jsonparser.h b/formats/jsonparser.h index be0fc6a..bb920fd 100644 --- a/formats/jsonparser.h +++ b/formats/jsonparser.h @@ -8,21 +8,21 @@ 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 = ""); 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 38046a3..dcba36f 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -13,9 +13,11 @@ #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" +#include "network/servercommunicator.h" #include @@ -37,6 +39,7 @@ GenericCore::GenericCore() { m_modelUndoStack = new QUndoStack(this); setupModels(); + setupServerConfiguration(); } GenericCore::~GenericCore() { qDebug() << "Destroying core..."; } @@ -103,7 +106,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.")); @@ -116,7 +119,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; @@ -132,6 +135,58 @@ 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); + + if (group == "Server") { + setupServerConfiguration(); + } +} + +bool GenericCore::isSyncServerSetup() const { + if (m_serverCommunicator) { + return true; + } else { + return false; + } +} + +void GenericCore::onSendItemTriggered(const QByteArray& jsonData) { + m_serverCommunicator->postItems(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 and apply changes (UUID,...) ? ; + m_mainModel->appendItems(jsonData); +} + +void GenericCore::onItemsFetchFailure(const QString errorString) { + emit displayStatusMessage(QString("Error: %1").arg(errorString)); +} + +void GenericCore::onPostRequestSuccessful(const QByteArray responseData) { + const QString message = m_mainModel->updateItemsFromJson(responseData); + emit displayStatusMessage(message); +} + +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); @@ -159,7 +214,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) @@ -188,3 +243,39 @@ QString GenericCore::getMaintenanceToolFilePath() const { const QString filePath = applicationDirPath + "/" + UPDATER_EXE; return filePath; } + +void GenericCore::setupServerConfiguration() { + m_serverCommunicator = make_unique(this); + /// request connections + 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, + &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); + connect(m_serverCommunicator.get(), &ServerCommunicator::deleteRequestSuccessful, this, + &GenericCore::onDeleteRequestSuccessful); + connect(m_serverCommunicator.get(), &ServerCommunicator::deleteRequestFailure, this, + &GenericCore::onDeleteRequestFailure); + + 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 a7df414..4bf30ea 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,8 +33,24 @@ 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; + + public slots: + void onSendItemTriggered(const QByteArray& jsonData); + void onItemsFetched(const QByteArray jsonData); + 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; @@ -46,6 +63,11 @@ class GenericCore : public QObject { void initModelData(); QString getMaintenanceToolFilePath() const; + + /// Network communication + std::unique_ptr m_serverCommunicator; + void setupServerConfiguration(); + void applyServerConfiguration(); }; #endif // GENERICCORE_H 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/generalsortfiltermodel.cpp b/model/generalsortfiltermodel.cpp index 0b1133e..1dd5cf3 100644 --- a/model/generalsortfiltermodel.cpp +++ b/model/generalsortfiltermodel.cpp @@ -23,6 +23,15 @@ 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); +} + void GeneralSortFilterModel::appendItems(const QByteArray& jsonDoc) { m_tableModel->appendItems(jsonDoc); } diff --git a/model/generalsortfiltermodel.h b/model/generalsortfiltermodel.h index 3fa4a8b..a5eade5 100644 --- a/model/generalsortfiltermodel.h +++ b/model/generalsortfiltermodel.h @@ -17,6 +17,9 @@ 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); public slots: void appendItems(const QByteArray& jsonDoc); diff --git a/model/metadata.h b/model/metadata.h index b0bb4b8..eadb1d0 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, @@ -13,26 +15,31 @@ enum UserRoles { InfoRole, AmountRole, FactorRole, - /// helper roles - ToStringRole + /// Non user facing + IdRole, + /// read only roles + ToStringRole, + ToJsonRole }; -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"; +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.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/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 08941b6..76e87a7 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()); @@ -28,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(); @@ -49,16 +50,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 { @@ -82,9 +83,12 @@ 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(); + case ToJsonRole: + return m_items.at(row)->toJsonObject(); } return QVariant(); @@ -149,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; @@ -168,6 +172,43 @@ 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_KEY_STRING, itemObject); + const QJsonDocument jsonDoc(rootObject); + return jsonDoc.toJson(QJsonDocument::Compact); +} + +QString TableModel::updateItemsFromJson(const QByteArray& jsonData) { + const QList valueList = JsonParser::toItemValuesList(jsonData, ITEM_KEY_STRING); + 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 false; + } else { + qInfo() << "Item found!"; + /// update existing item + setItemData(foundIndex, itemValues); + return true; + } +} + bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIndex) { if (parentIndex != QModelIndex()) { qWarning() << "Removing of child rows is not supported yet!"; @@ -191,7 +232,13 @@ 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 + qDebug() << "No items found in JSON document. Not adding anything..."; + return; + } insertItems(startPosition, valueList, parentIndex); } @@ -243,7 +290,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()); @@ -284,3 +331,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 4bd2dcd..a7d2089 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 @@ -38,6 +38,11 @@ class TableModel : public QAbstractTableModel { QJsonDocument getAllItemsAsJsonDoc() const; QList getItemsAsStringLists() const; + 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()) // override; @@ -66,6 +71,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/apiroutes.h b/network/apiroutes.h new file mode 100644 index 0000000..e89da3a --- /dev/null +++ b/network/apiroutes.h @@ -0,0 +1,12 @@ +#ifndef APIROUTES_H +#define APIROUTES_H + +#include + +// TODO add namespace + +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..7545e2e --- /dev/null +++ b/network/servercommunicator.cpp @@ -0,0 +1,106 @@ +#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(); +} + +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; + authenticationHeaders.append(QHttpHeaders::WellKnownHeader::ContentType, "application/json"); + 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)); + } + } + }); +} + +void ServerCommunicator::postItems(const QByteArray& jsonData) { + 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(responseData); + } else { + const QString message = QString("Error: %1").arg(reply->errorString()); + qDebug() << message; + emit postRequestFailure(message); + } + 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(); + }); +} + +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 new file mode 100644 index 0000000..3107604 --- /dev/null +++ b/network/servercommunicator.h @@ -0,0 +1,46 @@ +#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 setServerConfiguration(const QString url, const QString email, const QString password); + + public slots: + void fetchItems(); + void postItems(const QByteArray& jsonData); + void deleteItem(const QString& id); + + signals: + void urlChanged(); + + void itemsFetched(const QByteArray jsonDoc); + 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; + std::shared_ptr m_restManager; + std::shared_ptr m_serviceApi; + + QString m_email; + QString m_password; + // QString m_authToken; +}; + +#endif // SERVERCOMMUNICATOR_H