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..f7c3e13 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,55 @@ 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; +} + +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..9165def 100644 --- a/data/filehandler.h +++ b/data/filehandler.h @@ -1,17 +1,25 @@ #ifndef FILEHANDLER_H #define FILEHANDLER_H +#include + 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); + 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..814d151 --- /dev/null +++ b/formats/csvparser.cpp @@ -0,0 +1,141 @@ +#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>(); + } +} + +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 QHash 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; +} + +QHash CsvParser::getItemValuesForRow( + const QList& headerNames, + const QHash>& columnValueMap, + const int row) { + QHash 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..e4bb06f --- /dev/null +++ b/formats/csvparser.h @@ -0,0 +1,29 @@ +#ifndef CSVPARSER_H +#define CSVPARSER_H + +#include + +namespace rapidcsv { +class Document; +} + +class CsvParser { + public: + static QList> getItemsFromCSVFile(const QString& fileName); + + 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 QHash 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/genericcore.cpp b/genericcore.cpp index 86459f5..9b86bfb 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -101,8 +101,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 +108,20 @@ 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); +} + void GenericCore::setupModels() { m_mainModel = make_shared(m_modelUndoStack, this); // TODO add QAbstractItemModelTester diff --git a/genericcore.h b/genericcore.h index 671ded1..b4a9776 100644 --- a/genericcore.h +++ b/genericcore.h @@ -26,6 +26,7 @@ class GenericCore : public QObject { std::shared_ptr getModel() const; void saveItems(); + void importCSVFile(const QString& filePath); signals: void displayStatusMessage(QString message); 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/tablemodel.cpp b/model/tablemodel.cpp index 2ae6cdf..dd0dae0 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -169,18 +169,26 @@ 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); } diff --git a/model/tablemodel.h b/model/tablemodel.h index c7650f2..75f7bbd 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -43,6 +43,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 ***