diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f599c2..b47cfca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,9 @@ add_library(${TARGET_APP} STATIC model/commands/edititemcommand.h model/commands/edititemcommand.cpp data/filehandler.h data/filehandler.cpp model/metadata.h + formats/csvparser.h formats/csvparser.cpp + # 3rd party libraries + ../3rdParty/rapidcsv/src/rapidcsv.h ) include_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/data/filehandler.cpp b/data/filehandler.cpp index 5cd9bba..c405e55 100644 --- a/data/filehandler.cpp +++ b/data/filehandler.cpp @@ -5,6 +5,8 @@ #include #include +#include "../formats/csvparser.h" + bool FileHandler::saveToFile(const QJsonDocument& doc, const QString& fileName) { qDebug() << "saving file..."; QString path = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).at(0); @@ -29,25 +31,59 @@ bool FileHandler::saveToFile(const QJsonDocument& doc, const QString& fileName) } QByteArray FileHandler::loadJSONDataFromFile(const QString fileName) { - QByteArray jsonData; QFile file; QString path = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).at(0); file.setFileName(path + "/" + fileName); + + QPair fileContent = getFileContent(path + "/" + fileName); + + return fileContent.second; +} + +QList FileHandler::getItemValuesFromCSVFile(const QString& filePath) { + QList result; + QFile file; + file.setFileName(filePath); + if (file.exists()) { + result = CsvParser::getItemsFromCSVFile(filePath); + } + return result; +} + +bool FileHandler::exportToCSVFile(const QList& rows, const QString& filePath) { + return CsvParser::exportToCSVFile(rows, filePath); +} + +FileHandler::FileHandler() {} + +/** Tries to open the file specified by the file path & returns the content + * @brief FileHandler::getFileContent + * @param filePath + * @return Returns an error string (empty if successful) and the file content + */ +QPair FileHandler::getFileContent(const QString& filePath) { + QString errorString = ""; + + QByteArray fileContent; + QFile file; + file.setFileName(filePath); if (file.exists()) { qDebug() << "File found, reading content..."; const bool successfulOpened = file.open(QIODevice::ReadOnly | QIODevice::Text); if (successfulOpened) { // TODO learn and decide on the differences between "readAll" and using // streams - jsonData = file.readAll(); + fileContent = file.readAll(); file.close(); } else { - qWarning() << "File could not be opened!"; + errorString = "File could not be opened!"; + qWarning() << errorString; } } else { - qInfo() << "File not found. Returning empty result..."; + errorString = "File not found. Returning empty result..."; + qInfo() << errorString; } - return jsonData; -} -FileHandler::FileHandler() {} + const QPair result(errorString, fileContent); + return result; +} diff --git a/data/filehandler.h b/data/filehandler.h index aecf072..6fd6019 100644 --- a/data/filehandler.h +++ b/data/filehandler.h @@ -1,17 +1,28 @@ #ifndef FILEHANDLER_H #define FILEHANDLER_H +#include + +typedef QHash ModelItemValues; + class QJsonDocument; class QString; class QByteArray; class FileHandler { public: + /// JSON static bool saveToFile(const QJsonDocument& doc, const QString& fileName); static QByteArray loadJSONDataFromFile(const QString fileName); + /// CSV + static QList getItemValuesFromCSVFile(const QString& filePath); + static bool exportToCSVFile(const QList& rows, const QString& filePath); + private: explicit FileHandler(); + + static QPair getFileContent(const QString& filePath); }; #endif // FILEHANDLER_H diff --git a/formats/csvparser.cpp b/formats/csvparser.cpp new file mode 100644 index 0000000..365de1d --- /dev/null +++ b/formats/csvparser.cpp @@ -0,0 +1,158 @@ +#include "csvparser.h" + +#include +#include +#include + +#include "../../3rdParty/rapidcsv/src/rapidcsv.h" +#include "../model/metadata.h" + +using namespace rapidcsv; + +QList CsvParser::getItemsFromCSVFile(const QString& fileName) { + Document doc(fileName.toStdString()); + + const bool isCompatible = isCsvCompatible(doc); + if (isCompatible) { + const QList result = createListItemsFromCsvEntries(doc); + return result; + } else { + return QList(); + } +} + +bool CsvParser::exportToCSVFile(const QList& rows, const QString& filePath) { + Document doc(std::string(), LabelParams(0, -1)); + const QList headerNames = GET_HEADER_NAMES(); + for (int column = 0; column < headerNames.size(); ++column) { + doc.SetColumnName(column, headerNames.at(column).toStdString()); + } + for (int row = 0; row < rows.size(); ++row) { + QStringList rowValues = rows.at(row); + std::vector rowValueStrings; + for (int column = 0; column < rowValues.size(); ++column) { + rowValueStrings.push_back(rowValues.at(column).toStdString()); + } + doc.InsertRow(row, rowValueStrings); + } + doc.Save(filePath.toStdString()); + return true; +} + +CsvParser::CsvParser() {} + +/** A CSV file is compatible if the following is true: + * - there is a CSV column for every column in the table model + * (except there are optional columns defined in the model) + * @brief CsvParser::isCsvCompatible + * @param doc + * @return + */ +bool CsvParser::isCsvCompatible(const rapidcsv::Document& doc) { + qInfo() << "Checking CSV document for compatiblity..."; + const std::vector columnNames = doc.GetColumnNames(); + for (const QString& headerName : GET_HEADER_NAMES()) { + bool isHeaderNameFound = false; + if (std::find(columnNames.begin(), columnNames.end(), headerName) != columnNames.end()) { + qDebug() << QString("Header found in column names: %1").arg(headerName); + } else { + const QString errorString = + QString("Couldn't find header name '%1' in CSV file. Aborting...").arg(headerName); + qWarning() << errorString; + return false; + } + } + return true; +} + +QList CsvParser::createListItemsFromCsvEntries(const rapidcsv::Document& doc) { + QList result; + const int rowCount = doc.GetRowCount(); + const QList headerNames = GET_HEADER_NAMES(); + + /// get the values for all columns + QHash> columnValueMap = extractColumnValues(headerNames, doc); + + /// get item values for each row + for (int row = 0; row < rowCount; ++row) { + const ModelItemValues itemValues = getItemValuesForRow(headerNames, columnValueMap, row); + result.append(itemValues); + } + return result; +} + +QHash> CsvParser::extractColumnValues( + const QList headerNames, + const rapidcsv::Document& doc) { + QHash> columnValueMap; + for (const QString& columnName : headerNames) { + // NEXT add support for optional columns + // if (optionalCsvHeaderNames.contains(columnName)) { + // const std::vector columnNames = doc.GetColumnNames(); + // int columnIdx = doc.GetColumnIdx(columnName.toStdString()); + // if (columnIdx == -1) { + // continue; + // } + // } + const std::vector columnValues = + doc.GetColumn(columnName.toStdString()); + columnValueMap.insert(columnName, columnValues); + } + + return columnValueMap; +} + +ModelItemValues CsvParser::getItemValuesForRow( + const QList& headerNames, + const QHash>& columnValueMap, + const int row) { + ModelItemValues result; + for (const QString& columnName : headerNames) { + if (!columnValueMap.contains(columnName)) { + continue; + } + int role = ROLE_NAMES.key(columnName.toLatin1()); + std::string valueString = columnValueMap.value(columnName).at(row); + + QVariant value = parseItemValue(role, valueString); + if (value.isValid()) { + result[role] = value; + } + } + return result; +} + +QVariant CsvParser::parseItemValue(const int role, const std::string& valueString) { + QVariant result; + if (STRING_ROLES.contains(role)) { + /// string values + result = QString::fromStdString(valueString); + } else if (INT_ROLES.contains(role)) { + /// int values + + /// GetColumn crashed (probably because of the empty values) + /// so the strings will be processed later + const QString intAsString = QString::fromStdString(valueString); + result = intAsString.toInt(); + } else if (DOUBLE_ROLES.contains(role)) { + /// double values + const QString doubleAsString = QString::fromStdString(valueString); + double doubleValue; + if (doubleAsString.contains(',')) { + QLocale german(QLocale::German); + doubleValue = german.toDouble(doubleAsString); + } else { + doubleValue = doubleAsString.toDouble(); + } + result = doubleValue; + + // } else if (typeColumns.contains(columnName)) { + // // NEXT validate string is allowed + // values[role] = QString::fromStdString(columnValueMap.value(columnName).at(row)); + } else { + /// no type recognized for column + QString errorString = QString("Couldn't find value type for role '%1'").arg(role); + qCritical() << errorString; + } + return result; +} diff --git a/formats/csvparser.h b/formats/csvparser.h new file mode 100644 index 0000000..7695156 --- /dev/null +++ b/formats/csvparser.h @@ -0,0 +1,32 @@ +#ifndef CSVPARSER_H +#define CSVPARSER_H + +#include + +typedef QHash ModelItemValues; + +namespace rapidcsv { +class Document; +} + +class CsvParser { + public: + static QList getItemsFromCSVFile(const QString& fileName); + static bool exportToCSVFile(const QList& rows, const QString& filePath); + + private: + explicit CsvParser(); + + static bool isCsvCompatible(const rapidcsv::Document& doc); + static QList createListItemsFromCsvEntries(const rapidcsv::Document& doc); + static QHash> extractColumnValues( + const QList headerNames, + const rapidcsv::Document& doc); + static ModelItemValues getItemValuesForRow( + const QList& headerNames, + const QHash>& columnValueMap, + const int row); + static QVariant parseItemValue(const int role, const std::string& valueString); +}; + +#endif // CSVPARSER_H diff --git a/formats/jsonparser.cpp b/formats/jsonparser.cpp index 075f607..aa24722 100644 --- a/formats/jsonparser.cpp +++ b/formats/jsonparser.cpp @@ -5,9 +5,9 @@ #include "../model/metadata.h" -QList> JsonParser::toItemValuesList(const QByteArray& jsonData, - const QString& objectName) { - QList> result; +QList JsonParser::toItemValuesList(const QByteArray& jsonData, + const QString& objectName) { + QList result; if (jsonData.isEmpty()) { return result; @@ -16,14 +16,14 @@ QList> JsonParser::toItemValuesList(const QByteArray& jsonD QJsonArray itemArray = extractItemArray(jsonData, objectName); foreach (QJsonValue value, itemArray) { - QJsonObject itemJsonObject = value.toObject(); - QHash values = jsonObjectToItemValues(itemJsonObject); + QJsonObject itemJsonObject = value.toObject(); + ModelItemValues values = jsonObjectToItemValues(itemJsonObject); result.append(values); } return result; } -QByteArray JsonParser::itemValuesListToJson(const QList>& itemValuesList, +QByteArray JsonParser::itemValuesListToJson(const QList& itemValuesList, const QString& objectName) { QJsonDocument jsonDoc; QJsonObject rootObject; @@ -72,8 +72,8 @@ QJsonArray JsonParser::extractItemArray(const QByteArray& jsonData, const QStrin return itemArray; } -QHash JsonParser::jsonObjectToItemValues(const QJsonObject& itemJsonObject) { - QHash values; +ModelItemValues JsonParser::jsonObjectToItemValues(const QJsonObject& itemJsonObject) { + ModelItemValues values; QListIterator i(USER_FACING_ROLES); while (i.hasNext()) { diff --git a/formats/jsonparser.h b/formats/jsonparser.h index 702414d..be0fc6a 100644 --- a/formats/jsonparser.h +++ b/formats/jsonparser.h @@ -8,20 +8,22 @@ class QString; class QByteArray; class QJsonArray; +typedef QHash ModelItemValues; + using namespace std; class JsonParser { public: - static QList> toItemValuesList(const QByteArray& jsonData, - const QString& objectName = ""); - static QByteArray itemValuesListToJson(const QList>& itemValuesList, + static QList toItemValuesList(const QByteArray& jsonData, + const QString& objectName = ""); + static QByteArray itemValuesListToJson(const QList& itemValuesList, const QString& objectName = ""); private: explicit JsonParser(); static QJsonArray extractItemArray(const QByteArray& jsonData, const QString& objectName); - static QHash jsonObjectToItemValues(const QJsonObject& itemJsonObject); + static ModelItemValues jsonObjectToItemValues(const QJsonObject& itemJsonObject); static pair getKeyValuePair(const QJsonObject& itemJsonObject, const int role); }; diff --git a/genericcore.cpp b/genericcore.cpp index 86459f5..34ef9dd 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -76,12 +76,10 @@ bool GenericCore::isApplicationUpdateAvailable() { return true; } -void GenericCore::triggerApplicationUpdate() { - // TODO include cleaness of undo stack - // if (!m_undoStack->isClean()) { - // saveItems(); - // } - // QStringList args("update componentA componentB"); +void GenericCore::triggerApplicationUpdate(const bool saveChanges) { + if (saveChanges && !m_modelUndoStack->isClean()) { + saveItems(); + } QStringList args("--start-updater"); QString toolFilePath = getMaintenanceToolFilePath(); QProcess::startDetached(toolFilePath, args); @@ -101,8 +99,6 @@ void GenericCore::saveItems() { const QJsonDocument doc = m_mainModel->getAllItemsAsJsonDoc(); const bool successfulSave = FileHandler::saveToFile(doc, ITEM_FILE_NAME); if (successfulSave) { - // QStringList completedTaskStrings = m_model->completedTasks(); - // appendCompletedTasksToFile(completedTaskStrings, "completed.txt"); m_modelUndoStack->setClean(); emit displayStatusMessage(QString("Items saved.")); } else { @@ -110,6 +106,26 @@ void GenericCore::saveItems() { } } +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 + if (itemValuesList.isEmpty()) { + qDebug() << "No items found. Doing nothing..."; + return; + } + // qDebug() << "CSV file content:" << itemValuesList; + m_mainModel->insertItems(m_mainModel->rowCount(), itemValuesList); +} + +bool GenericCore::exportCSVFile(const QString& filePath) { + qInfo() << "exporting items to CSV..."; + qDebug() << "filePath:" << filePath; + const QList itemsAsStringLists = m_mainModel->getItemsAsStringLists(); + return FileHandler::exportToCSVFile(itemsAsStringLists, filePath); +} + void GenericCore::setupModels() { m_mainModel = make_shared(m_modelUndoStack, this); // TODO add QAbstractItemModelTester diff --git a/genericcore.h b/genericcore.h index 671ded1..b25135c 100644 --- a/genericcore.h +++ b/genericcore.h @@ -20,12 +20,14 @@ class GenericCore : public QObject { void sayHello() const; bool isApplicationUpdateAvailable(); - void triggerApplicationUpdate(); + void triggerApplicationUpdate(const bool saveChanges); QUndoStack* getModelUndoStack() const; std::shared_ptr getModel() const; void saveItems(); + void importCSVFile(const QString& filePath); + bool exportCSVFile(const QString& filePath); signals: void displayStatusMessage(QString message); diff --git a/model/commands/insertrowscommand.cpp b/model/commands/insertrowscommand.cpp index 231ce29..99f25d7 100644 --- a/model/commands/insertrowscommand.cpp +++ b/model/commands/insertrowscommand.cpp @@ -5,9 +5,9 @@ #include "../tablemodel.h" InsertRowsCommand::InsertRowsCommand(TableModel* model, - int startRow, - QList > valueList, - QUndoCommand* parent) + int startRow, + QList valueList, + QUndoCommand* parent) : QUndoCommand(parent) , m_tableModel(model) , m_startRow(startRow) diff --git a/model/commands/insertrowscommand.h b/model/commands/insertrowscommand.h index 0341226..ddc5617 100644 --- a/model/commands/insertrowscommand.h +++ b/model/commands/insertrowscommand.h @@ -3,6 +3,8 @@ #include +typedef QHash ModelItemValues; + class TableModel; class InsertRowsCommand : public QUndoCommand { @@ -11,9 +13,9 @@ class InsertRowsCommand : public QUndoCommand { /// Using simple pointer to model because there was a crash when closing the application with an /// unclean undo stack InsertRowsCommand(TableModel* model, - int startRow, - QList > valueList, - QUndoCommand* parent = nullptr); + int startRow, + QList valueList, + QUndoCommand* parent = nullptr); /// QUndoCommand interface void undo() override; @@ -22,7 +24,7 @@ class InsertRowsCommand : public QUndoCommand { private: TableModel* m_tableModel; const int m_startRow; - const QList > m_valueList; + const QList m_valueList; }; #endif // INSERTROWSCOMMAND_H diff --git a/model/commands/removerowscommand.cpp b/model/commands/removerowscommand.cpp index 487a916..2e2a026 100644 --- a/model/commands/removerowscommand.cpp +++ b/model/commands/removerowscommand.cpp @@ -20,7 +20,7 @@ RemoveRowsCommand::RemoveRowsCommand(TableModel* model, const int rowPosition = startRow + row; QModelIndex index = m_tableModel->index(rowPosition, 0); - QHash values = m_tableModel->getItemValues(index); + ModelItemValues values = m_tableModel->getItemValues(index); m_valueList.append(values); } diff --git a/model/commands/removerowscommand.h b/model/commands/removerowscommand.h index 308f78b..6703aab 100644 --- a/model/commands/removerowscommand.h +++ b/model/commands/removerowscommand.h @@ -5,6 +5,8 @@ class TableModel; +typedef QHash ModelItemValues; + class RemoveRowsCommand : public QUndoCommand { public: // TODO don't use simple pointer to model @@ -22,7 +24,7 @@ class RemoveRowsCommand : public QUndoCommand { private: TableModel* m_tableModel; const int m_startRow; - QList> m_valueList; + QList m_valueList; }; #endif // REMOVEROWSCOMMAND_H diff --git a/model/metadata.h b/model/metadata.h index 580d931..97f7d4d 100644 --- a/model/metadata.h +++ b/model/metadata.h @@ -50,5 +50,13 @@ static int GET_ROLE_FOR_COLUMN(const int column) { break; } } +static QList GET_HEADER_NAMES() { + QList result; + for (const UserRoles& role : USER_FACING_ROLES) { + const QString headerName = ROLE_NAMES.value(role); + result.append(headerName); + } + return result; +} #endif // METADATA_H diff --git a/model/modelitem.cpp b/model/modelitem.cpp index 7abf6bf..32bc401 100644 --- a/model/modelitem.cpp +++ b/model/modelitem.cpp @@ -5,7 +5,7 @@ #include #include -ModelItem::ModelItem(const QHash values) +ModelItem::ModelItem(const ModelItemValues values) : m_values(values) {} QVariant ModelItem::data(int role) const { return m_values.value(role); } diff --git a/model/modelitem.h b/model/modelitem.h index 2f3e037..9078c33 100644 --- a/model/modelitem.h +++ b/model/modelitem.h @@ -3,9 +3,11 @@ #include +typedef QHash ModelItemValues; + class ModelItem { public: - ModelItem(const QHash values); + ModelItem(const ModelItemValues values); QVariant data(int role) const; bool setData(const QVariant& value, int role); diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 2ae6cdf..4895e9a 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -120,8 +120,8 @@ bool TableModel::setItemData(const QModelIndex& index, const QMap return false; } -QHash TableModel::getItemValues(const QModelIndex& index) const { - QHash values; +ModelItemValues TableModel::getItemValues(const QModelIndex& index) const { + ModelItemValues values; QListIterator i(USER_FACING_ROLES); while (i.hasNext()) { @@ -146,6 +146,19 @@ QJsonDocument TableModel::getAllItemsAsJsonDoc() const { return doc; } +QList TableModel::getItemsAsStringLists() const { + QList result; + foreach (shared_ptr item, m_items) { + QStringList valueList; + for (int column = 0; column < columnCount(); ++column) { + QString value = item->data(GET_ROLE_FOR_COLUMN(column)).toString(); + valueList.append(value); + } + result.append(valueList); + } + return result; +} + bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIndex) { if (parentIndex != QModelIndex()) { qWarning() << "Removing of child rows is not supported yet!"; @@ -169,22 +182,29 @@ 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); + + insertItems(startPosition, valueList, parentIndex); +} + +void TableModel::insertItems(int startPosition, + const QList& itemValuesList, + const QModelIndex& parentIndex) { qInfo() << "Inserting item(s) into model..."; if (parentIndex != QModelIndex()) { - qWarning() << "Using invalid parent index (no child support for now)"; + qWarning() + << "Using invalid parent index (no child support for now)! Using root index as parent..."; } if (startPosition == -1 || startPosition > m_items.size()) { /// Appending item(s) startPosition = m_items.size(); } - QList> valueList = JsonParser::toItemValuesList(jsonDoc, ITEM_KEY_STRING); - - InsertRowsCommand* insertCommand = new InsertRowsCommand(this, startPosition, valueList); + InsertRowsCommand* insertCommand = new InsertRowsCommand(this, startPosition, itemValuesList); m_undoStack->push(insertCommand); } -void TableModel::execInsertItems(const int firstRow, const QList> valueList) { +void TableModel::execInsertItems(const int firstRow, const QList valueList) { const int nRows = valueList.size(); qDebug() << "Inserting" << nRows << "items..."; diff --git a/model/tablemodel.h b/model/tablemodel.h index c7650f2..4bd2dcd 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -8,6 +8,8 @@ class ModelItem; using namespace std; +typedef QHash ModelItemValues; + class TableModel : public QAbstractTableModel { Q_OBJECT @@ -32,8 +34,9 @@ class TableModel : public QAbstractTableModel { bool setData(const QModelIndex& index, const QVariant& value, int role) override; bool setItemData(const QModelIndex& index, const QMap& roles) override; - QHash getItemValues(const QModelIndex& index) const; + ModelItemValues getItemValues(const QModelIndex& index) const; QJsonDocument getAllItemsAsJsonDoc() const; + QList getItemsAsStringLists() const; public slots: // bool insertRows(int position, int rows, const QModelIndex& parentIndex = QModelIndex()) @@ -43,6 +46,9 @@ class TableModel : public QAbstractTableModel { void insertItems(int startPosition, const QByteArray& jsonDoc, const QModelIndex& parentIndex = QModelIndex()); + void insertItems(int startPosition, + const QList& itemValuesList, + const QModelIndex& parentIndex = QModelIndex()); private: /// *** members *** @@ -52,7 +58,7 @@ class TableModel : public QAbstractTableModel { /// *** functions *** /// undo/redo functions - void execInsertItems(const int firstRow, const QList> valueList); + void execInsertItems(const int firstRow, const QList valueList); void execRemoveItems(const int firstRow, const int nRows); void execEditItemData(const int row, const QMap& changedValues);