diff --git a/src/console/jobs/handshakejob.cpp b/src/console/jobs/handshakejob.cpp index dea860c..cd66a98 100644 --- a/src/console/jobs/handshakejob.cpp +++ b/src/console/jobs/handshakejob.cpp @@ -1,76 +1,77 @@ /* Copyright (C) 2017 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "handshakejob.h" #include #include #include #include #include #include using namespace KUserFeedback::Console; HandshakeJob::HandshakeJob(RESTClient* restClient, QObject* parent) : Job(parent) , m_restClient(restClient) { auto reply = RESTApi::checkSchema(restClient); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { emit info(tr("Connected to %1.").arg(m_restClient->serverInfo().url().toString())); processResponse(reply); } else { emitError(reply->errorString()); } + reply->deleteLater(); }); connect(reply, &QNetworkReply::redirected, this, [this, reply](const auto &url) { auto s = m_restClient->serverInfo(); auto u = url; auto p = u.path(); p.remove(QLatin1String("analytics/check_schema")); u.setPath(p); s.setUrl(u); m_restClient->setServerInfo(s); }); } HandshakeJob::~HandshakeJob() { } void HandshakeJob::processResponse(QNetworkReply* reply) { const auto doc = QJsonDocument::fromJson(reply->readAll()); const auto obj = doc.object(); const auto protoVer = obj.value(QLatin1String("protocolVersion")).toInt(); if (protoVer != 2) { emitError(tr("Incompatible protocol: %1.").arg(protoVer)); return; } const auto prevSchema = obj.value(QLatin1String("previousSchemaVersion")).toInt(); const auto curSchema = obj.value(QLatin1String("currentSchemaVersion")).toInt(); if (prevSchema != curSchema) emit info(tr("Updated database schema from version %1 to %2.").arg(prevSchema).arg(curSchema)); m_restClient->setConnected(true); emitFinished(); } diff --git a/src/console/jobs/productexportjob.cpp b/src/console/jobs/productexportjob.cpp index 5c899e0..ed4e519 100644 --- a/src/console/jobs/productexportjob.cpp +++ b/src/console/jobs/productexportjob.cpp @@ -1,128 +1,131 @@ /* Copyright (C) 2017 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "productexportjob.h" #include #include #include #include using namespace KUserFeedback::Console; ProductExportJob::ProductExportJob(const QString& productId, const QString& destination, RESTClient* restClient, QObject* parent) : Job(parent) , m_dest(destination) , m_restClient(restClient) { Q_ASSERT(m_restClient); Q_ASSERT(m_restClient->isConnected()); auto reply = RESTApi::listProducts(restClient); connect(reply, &QNetworkReply::finished, this, [this, productId, reply]() { + reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { deleteLater(); return; } const auto products = Product::fromJson(reply->readAll()); const auto it = std::find_if(products.begin(), products.end(), [productId](const auto &p) { return p.name() == productId; }); if (it == products.end()) { emitError(tr("Product not found.")); } else { m_product = *it; doExportSchema(); } }); } ProductExportJob::ProductExportJob(const Product& product, const QString& destination, RESTClient* restClient, QObject* parent) : Job(parent) , m_product(product) , m_dest(destination) , m_restClient(restClient) { Q_ASSERT(m_restClient); Q_ASSERT(m_restClient->isConnected()); doExportSchema(); } ProductExportJob::~ProductExportJob() = default; void ProductExportJob::doExportSchema() { Q_ASSERT(m_product.isValid()); QFile f(destination() + QLatin1Char('/') + m_product.name() + QLatin1String(".schema")); if (!f.open(QFile::WriteOnly)) { emitError(tr("Could not open file: %1").arg(f.errorString())); return; } f.write(m_product.toJson()); doExportSurveys(); } void ProductExportJob::doExportSurveys() { auto reply = RESTApi::listSurveys(m_restClient, m_product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { deleteLater(); return; } QFile f(destination() + QLatin1Char('/') + m_product.name() + QLatin1String(".surveys")); if (!f.open(QFile::WriteOnly)) { emitError(tr("Could not open file: %1").arg(f.errorString())); return; } f.write(reply->readAll()); doExportData(); }); } void ProductExportJob::doExportData() { auto reply = RESTApi::listSamples(m_restClient, m_product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { deleteLater(); return; } QFile f(destination() + QLatin1Char('/') + m_product.name() + QLatin1String(".data")); if (!f.open(QFile::WriteOnly)) { emitError(tr("Could not open file: %1").arg(f.errorString())); return; } const auto samples = Sample::fromJson(reply->readAll(), m_product); f.write(Sample::toJson(samples, m_product)); emitFinished(); }); } QString ProductExportJob::destination() const { QDir dest(m_dest); Q_ASSERT(dest.exists()); dest.mkpath(m_product.name()); dest.cd(m_product.name()); return dest.absolutePath(); } diff --git a/src/console/jobs/productimportjob.cpp b/src/console/jobs/productimportjob.cpp index c9f0878..88e66ba 100644 --- a/src/console/jobs/productimportjob.cpp +++ b/src/console/jobs/productimportjob.cpp @@ -1,120 +1,123 @@ /* Copyright (C) 2017 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "productimportjob.h" #include #include #include #include #include #include #include using namespace KUserFeedback::Console; ProductImportJob::ProductImportJob(const QString& source, RESTClient* restClient, QObject* parent) : Job(parent) , m_source(source) , m_restClient(restClient) { Q_ASSERT(m_restClient->isConnected()); doImportSchema(); } ProductImportJob::~ProductImportJob() = default; void ProductImportJob::doImportSchema() { QDir source(m_source); Q_ASSERT(source.exists()); QFile f(source.absoluteFilePath(source.dirName() + QLatin1String(".schema"))); if (!f.open(QFile::ReadOnly)) { emitError(tr("Unable to open file: %1").arg(f.errorString())); return; } const auto products = Product::fromJson(f.readAll()); if (products.size() != 1) { emitError(tr("Invalid product schema file.")); return; } m_product = products.at(0); auto reply = RESTApi::createProduct(m_restClient, m_product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { deleteLater(); return; } doImportSurveys(); }); } void ProductImportJob::doImportSurveys() { QDir source(m_source); QFile f(source.absoluteFilePath(source.dirName() + QLatin1String(".surveys"))); if (!f.open(QFile::ReadOnly)) { doImportData(); return; } const auto surveys = Survey::fromJson(f.readAll()); if (surveys.isEmpty()) { doImportData(); return; } for (const auto &s : surveys) { ++m_jobCount; auto reply = RESTApi::createSurvey(m_restClient, m_product, s); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); --m_jobCount; if (reply->error() != QNetworkReply::NoError) { deleteLater(); return; } if (m_jobCount == 0) doImportData(); }); } } void ProductImportJob::doImportData() { QDir source(m_source); QFile f(source.absoluteFilePath(source.dirName() + QLatin1String(".data"))); if (!f.open(QFile::ReadOnly)) { emitFinished(); return; } const auto samples = Sample::fromJson(f.readAll(), m_product); if (samples.isEmpty()) { emitFinished(); return; } auto reply = RESTApi::addSamples(m_restClient, m_product, samples); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); if (reply->error() == QNetworkReply::NoError) emitFinished(); }); } diff --git a/src/console/mainwindow.cpp b/src/console/mainwindow.cpp index 621cc6e..ba137d0 100644 --- a/src/console/mainwindow.cpp +++ b/src/console/mainwindow.cpp @@ -1,372 +1,374 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "mainwindow.h" #include "ui_mainwindow.h" #include "helpcontroller.h" #include "connectdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KUserFeedback::Console; MainWindow::MainWindow() : ui(new Ui::MainWindow), m_restClient(new RESTClient(this)), m_productModel(new ProductModel(this)), m_feedbackProvider(new KUserFeedback::Provider(this)) { ui->setupUi(this); setWindowIcon(QIcon::fromTheme(QStringLiteral("search"))); addView(ui->surveyEditor, ui->menuSurvey); addView(ui->schemaEdit, ui->menuSchema); addView(ui->analyticsView, ui->menuAnalytics); ui->productListView->setModel(m_productModel); ui->productListView->addActions({ ui->actionAddProduct, ui->actionDeleteProduct }); connect(m_restClient, &RESTClient::errorMessage, this, &MainWindow::logError); m_productModel->setRESTClient(m_restClient); ui->actionViewAnalytics->setData(QVariant::fromValue(ui->analyticsView)); ui->actionViewSurveys->setData(QVariant::fromValue(ui->surveyEditor)); ui->actionViewSchema->setData(QVariant::fromValue(ui->schemaEdit)); auto viewGroup = new QActionGroup(this); viewGroup->setExclusive(true); viewGroup->addAction(ui->actionViewAnalytics); viewGroup->addAction(ui->actionViewSurveys); viewGroup->addAction(ui->actionViewSchema); connect(viewGroup, &QActionGroup::triggered, this, [this](QAction *action) { auto view = action->data().value(); if (ui->viewStack->currentWidget() == ui->schemaEdit && ui->schemaEdit->isDirty() && view != ui->schemaEdit) { const auto r = QMessageBox::critical(this, tr("Unsaved Schema Changes"), tr("You have unsaved changes in the schema editor. Do you really want to close it and discard your changes?"), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (r != QMessageBox::Discard) { ui->actionViewSchema->setChecked(true); return; } } ui->viewStack->setCurrentWidget(view); }); ui->actionViewAnalytics->setChecked(true); connect(ui->actionConnectToServer, &QAction::triggered, this, [this]() { QSettings settings; auto info = ServerInfo::load(settings.value(QStringLiteral("LastServerInfo")).toString()); ConnectDialog dlg(this); dlg.addRecentServerInfos(ServerInfo::allServerInfoNames()); dlg.setServerInfo(info); if (dlg.exec()) { info = dlg.serverInfo(); info.save(); settings.setValue(QStringLiteral("LastServerInfo"), info.name()); connectToServer(info); } }); connect(ui->actionAddProduct, &QAction::triggered, this, &MainWindow::createProduct); connect(ui->actionDeleteProduct, &QAction::triggered, this, &MainWindow::deleteProduct); connect(ui->actionImportProduct, &QAction::triggered, this, &MainWindow::importProduct); connect(ui->actionExportProduct, &QAction::triggered, this, &MainWindow::exportProduct); connect(ui->schemaEdit, &SchemaEditor::productChanged, m_productModel, &ProductModel::reload); ui->actionQuit->setShortcut(QKeySequence::Quit); connect(ui->actionQuit, &QAction::triggered, this, &QMainWindow::close); ui->menuWindow->addAction(ui->productsDock->toggleViewAction()); ui->menuWindow->addAction(ui->logDock->toggleViewAction()); ui->actionUserManual->setEnabled(HelpController::isAvailable()); ui->actionUserManual->setShortcut(QKeySequence::HelpContents); connect(ui->actionUserManual, &QAction::triggered, this, []() { HelpController::openContents(); }); ui->actionContribute->setVisible(m_feedbackProvider->isEnabled()); connect(ui->actionContribute, &QAction::triggered, this, [this]() { FeedbackConfigDialog dlg(this); dlg.setFeedbackProvider(m_feedbackProvider); dlg.exec(); }); connect(ui->actionAbout, &QAction::triggered, this, [this]() { QMessageBox::about(this, tr("About User Feedback Console"), tr( "Version: %1\n" "License: LGPLv2+\n" "Copyright ⓒ 2017 Volker Krause " ).arg(QStringLiteral(KUSERFEEDBACK_VERSION_STRING))); }); connect(ui->productListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection&, const QItemSelection &deselected) { static bool recursionGuard = false; if (recursionGuard) return; if (ui->viewStack->currentWidget() == ui->schemaEdit && ui->schemaEdit->isDirty()) { const auto r = QMessageBox::critical(this, tr("Unsaved Schema Changes"), tr("You have unsaved changes in the schema editor, do you really want to open another product and discard your changes?"), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (r != QMessageBox::Discard) { QScopedValueRollback guard(recursionGuard, true); ui->productListView->selectionModel()->select(deselected, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); return; } } productSelected(); }); connect(m_productModel, &QAbstractItemModel::dataChanged, this, &MainWindow::productSelected); connect(ui->viewStack, &QStackedWidget::currentChanged, this, &MainWindow::updateActions); connect(ui->viewStack, &QStackedWidget::currentChanged, this, [viewGroup](int index) { viewGroup->actions().at(index)->setChecked(true); }); updateActions(); QSettings settings; settings.beginGroup(QStringLiteral("MainWindow")); restoreGeometry(settings.value(QStringLiteral("Geometry")).toByteArray()); restoreState(settings.value(QStringLiteral("State")).toByteArray()); ui->viewStack->setCurrentIndex(settings.value(QStringLiteral("CurrentView"), 0).toInt()); QTimer::singleShot(0, ui->actionConnectToServer, &QAction::trigger); m_feedbackProvider->setFeedbackServer(QUrl(QStringLiteral("https://feedback.volkerkrause.eu"))); m_feedbackProvider->setSubmissionInterval(1); auto viewModeSource = new KUserFeedback::PropertyRatioSource(ui->viewStack, "currentIndex", QStringLiteral("viewRatio")); viewModeSource->setDescription(tr("Usage ratio of the analytics view, survey editor and schema editor.")); viewModeSource->addValueMapping(0, QStringLiteral("analytics")); viewModeSource->addValueMapping(1, QStringLiteral("surveyEditor")); viewModeSource->addValueMapping(2, QStringLiteral("schemaEditor")); viewModeSource->setTelemetryMode(Provider::DetailedUsageStatistics); m_feedbackProvider->addDataSource(viewModeSource); m_feedbackProvider->addDataSource(new ApplicationVersionSource); m_feedbackProvider->addDataSource(new PlatformInfoSource); m_feedbackProvider->addDataSource(new QtVersionSource); m_feedbackProvider->addDataSource(new StartCountSource); m_feedbackProvider->addDataSource(new UsageTimeSource); m_feedbackProvider->setEncouragementDelay(60); m_feedbackProvider->setEncouragementInterval(5); m_feedbackProvider->setApplicationStartsUntilEncouragement(5); m_feedbackProvider->setApplicationUsageTimeUntilEncouragement(600); // 10 mins auto notifyPopup = new KUserFeedback::NotificationPopup(this); notifyPopup->setFeedbackProvider(m_feedbackProvider); } MainWindow::~MainWindow() { QSettings settings; settings.beginGroup(QStringLiteral("MainWindow")); settings.setValue(QStringLiteral("State"), saveState()); settings.setValue(QStringLiteral("Geometry"), saveGeometry()); settings.setValue(QStringLiteral("CurrentView"), ui->viewStack->currentIndex()); } template void MainWindow::addView(T *view, QMenu *menu) { for (auto action : view->actions()) menu->addAction(action); view->setRESTClient(m_restClient); connect(view, &T::logMessage, this, &MainWindow::logMessage); } void MainWindow::connectToServer(const ServerInfo& info) { m_restClient->setServerInfo(info); auto job = new HandshakeJob(m_restClient, this); connect(job, &Job::destroyed, this, &MainWindow::updateActions); connect(job, &Job::error, this, [this](const QString &msg) { logError(msg); QMessageBox::critical(this, tr("Connection Failure"), tr("Failed to connect to server: %1").arg(msg)); }); connect(job, &Job::info, this, &MainWindow::logMessage); } void MainWindow::createProduct() { const auto name = QInputDialog::getText(this, tr("Add New Product"), tr("Product Identifier:")); if (name.isEmpty()) return; Product product; product.setName(name); auto reply = RESTApi::createProduct(m_restClient, product); connect(reply, &QNetworkReply::finished, this, [this, reply, name]() { + reply->deleteLater(); if (reply->error() == QNetworkReply::NoError) { logMessage(QString::fromUtf8(reply->readAll())); m_productModel->reload(); } }); } void MainWindow::deleteProduct() { auto sel = ui->productListView->selectionModel()->selectedRows(); if (sel.isEmpty()) return; const auto product = sel.first().data(ProductModel::ProductRole).value(); const auto mb = QMessageBox::critical(this, tr("Delete Product"), tr("Do you really want to delete product %1 with all its data?").arg(product.name()), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (mb != QMessageBox::Discard) return; auto reply = RESTApi::deleteProduct(m_restClient, product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); if (reply->error() == QNetworkReply::NoError) { logMessage(QString::fromUtf8(reply->readAll())); } m_productModel->reload(); }); } void MainWindow::importProduct() { const auto fileName = QFileDialog::getExistingDirectory(this, tr("Import Product")); if (fileName.isEmpty()) return; QFileInfo fi(fileName); if (!fi.exists() || !fi.isDir() || !fi.isReadable()) { QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file.")); return; } auto job = new ProductImportJob(fileName, m_restClient, this); connect(job, &Job::error, this, [this](const auto &msg) { QMessageBox::critical(this, tr("Import Failed"), tr("Import error: %1").arg(msg)); }); connect(job, &Job::finished, this, [this]() { logMessage(tr("Product imported successfully.")); m_productModel->reload(); }); } void MainWindow::exportProduct() { if (!selectedProduct().isValid()) return; const auto fileName = QFileDialog::getExistingDirectory(this, tr("Export Product")); if (fileName.isEmpty()) return; QFileInfo fi(fileName); if (!fi.exists() || !fi.isDir() || !fi.isWritable()) { QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file.")); return; } auto job = new ProductExportJob(selectedProduct(), fileName, m_restClient, this); connect(job, &Job::error, this, [this](const auto &msg) { QMessageBox::critical(this, tr("Export Failed"), tr("Export error: %1").arg(msg)); }); connect(job, &Job::finished, this, [this]() { logMessage(tr("Product exported successfully.")); }); } void MainWindow::productSelected() { const auto product = selectedProduct(); ui->surveyEditor->setProduct(product); ui->schemaEdit->setProduct(product); ui->analyticsView->setProduct(product); updateActions(); } void MainWindow::logMessage(const QString& msg) { ui->logWidget->appendPlainText(msg); } void MainWindow::logError(const QString& msg) { ui->logWidget->appendHtml( QStringLiteral("") + msg + QStringLiteral("")); } Product MainWindow::selectedProduct() const { const auto selection = ui->productListView->selectionModel()->selectedRows(); if (selection.isEmpty()) return {}; const auto idx = selection.first(); return idx.data(ProductModel::ProductRole).value(); } void MainWindow::updateActions() { // product action state ui->actionAddProduct->setEnabled(m_restClient->isConnected()); ui->actionDeleteProduct->setEnabled(selectedProduct().isValid()); ui->actionImportProduct->setEnabled(m_restClient->isConnected()); ui->actionExportProduct->setEnabled(selectedProduct().isValid()); // deactivate menus of the inactive views ui->menuAnalytics->setEnabled(ui->viewStack->currentWidget() == ui->analyticsView); ui->menuSurvey->setEnabled(ui->viewStack->currentWidget() == ui->surveyEditor); ui->menuSchema->setEnabled(ui->viewStack->currentWidget() == ui->schemaEdit); } void MainWindow::closeEvent(QCloseEvent* event) { if (ui->schemaEdit->isDirty()) { const auto r = QMessageBox::critical(this, tr("Unsaved Changes"), tr("There are unsaved changes in the schema editor. Do you want to discard them and close the application?"), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (r != QMessageBox::Discard) { event->ignore(); return; } } QMainWindow::closeEvent(event); } diff --git a/src/console/model/datamodel.cpp b/src/console/model/datamodel.cpp index babcafb..d75a1c3 100644 --- a/src/console/model/datamodel.cpp +++ b/src/console/model/datamodel.cpp @@ -1,173 +1,174 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "datamodel.h" #include #include #include #include #include #include using namespace KUserFeedback::Console; static QString mapToString(const QVariantMap &map) { QStringList l; l.reserve(map.size()); for (auto it = map.begin(); it != map.end(); ++it) { if (it.value().type() == QVariant::Map) l.push_back(it.key() + QLatin1String(": {") + mapToString(it.value().toMap()) + QLatin1Char('}')); else l.push_back(it.key() + QLatin1String(": ") + it.value().toString()); } return l.join(QLatin1String(", ")); } static QString listToString(const QVariantList &list) { QStringList l; l.reserve(list.size()); for (const auto &v : list) l.push_back(mapToString(v.toMap())); return QLatin1String("[{") + l.join(QLatin1String("}, {")) + QLatin1String("}]"); } QString DataModel::Column::name() const { if (entry.dataType() == SchemaEntry::Scalar) return entry.name() + QLatin1Char('.') + element.name(); return entry.name(); } DataModel::DataModel(QObject *parent) : QAbstractTableModel(parent) { } DataModel::~DataModel() = default; void DataModel::setRESTClient(RESTClient* client) { Q_ASSERT(client); m_restClient = client; connect(client, &RESTClient::clientConnected, this, &DataModel::reload); if (client->isConnected()) reload(); } Product DataModel::product() const { return m_product; } void DataModel::setProduct(const Product& product) { beginResetModel(); m_product = product; m_columns.clear(); for (const auto &entry : product.schema()) { if (entry.dataType() == SchemaEntry::Scalar) { for (const auto &elem : entry.elements()) m_columns.push_back({entry, elem}); } else { m_columns.push_back({entry, {}}); } } m_data.clear(); reload(); endResetModel(); } void DataModel::setSamples(const QVector &samples) { beginResetModel(); m_data = samples; std::sort(m_data.begin(), m_data.end(), [](const Sample &lhs, const Sample &rhs) { return lhs.timestamp() < rhs.timestamp(); }); endResetModel(); } void DataModel::reload() { if (!m_restClient || !m_restClient->isConnected() || !m_product.isValid()) return; auto reply = RESTApi::listSamples(m_restClient, m_product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { const auto samples = Sample::fromJson(reply->readAll(), m_product); setSamples(samples); } + reply->deleteLater(); }); } int DataModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_columns.size() + 1; } int DataModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_data.size(); } QVariant DataModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return {}; if (role == Qt::DisplayRole) { const auto sample = m_data.at(index.row()); if (index.column() == 0) return sample.timestamp(); const auto col = m_columns.at(index.column() - 1); const auto v = sample.value(col.name()); switch (col.entry.dataType()) { case SchemaEntry::Scalar: return v; case SchemaEntry::List: return listToString(v.toList()); case SchemaEntry::Map: return mapToString(v.toMap()); } } else if (role == SampleRole) { return QVariant::fromValue(m_data.at(index.row())); } else if (role == AllSamplesRole) { return QVariant::fromValue(m_data); } return {}; } QVariant DataModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && m_product.isValid()) { if (section == 0) return tr("Timestamp"); const auto col = m_columns.at(section - 1); return QString(col.name()); } return QAbstractTableModel::headerData(section, orientation, role); } diff --git a/src/console/model/productmodel.cpp b/src/console/model/productmodel.cpp index 740c9ae..48afa6a 100644 --- a/src/console/model/productmodel.cpp +++ b/src/console/model/productmodel.cpp @@ -1,142 +1,143 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "productmodel.h" #include #include #include using namespace KUserFeedback::Console; ProductModel::ProductModel(QObject *parent) : QAbstractListModel(parent) { } ProductModel::~ProductModel() = default; void ProductModel::setRESTClient(RESTClient* client) { if (client != m_restClient) clear(); Q_ASSERT(client); m_restClient = client; connect(m_restClient, &RESTClient::clientConnected, this, &ProductModel::reload); reload(); } void ProductModel::clear() { if (m_products.isEmpty()) return; beginRemoveRows({}, 0, m_products.size() - 1); m_products.clear(); endRemoveRows(); } void ProductModel::reload() { if (!m_restClient || !m_restClient->isConnected()) return; auto reply = RESTApi::listProducts(m_restClient); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { auto json = reply->readAll(); mergeProducts(Product::fromJson(json)); } + reply->deleteLater(); }); } int ProductModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_products.size(); } QVariant ProductModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; switch (role) { case Qt::DisplayRole: return m_products.at(index.row()).name(); case ProductRole: return QVariant::fromValue(m_products.at(index.row())); } return {}; } QVariant ProductModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case 0: return tr("Products"); } } return QAbstractListModel::headerData(section, orientation, role); } void ProductModel::mergeProducts(QVector &&products) { std::sort(products.begin(), products.end(), [](const Product &lhs, const Product &rhs) { Q_ASSERT(lhs.isValid()); Q_ASSERT(rhs.isValid()); return lhs.name() < rhs.name(); }); auto newIt = products.cbegin(); auto it = m_products.begin(); while (it != m_products.end() && newIt != products.cend()) { const auto row = std::distance(m_products.begin(), it); if ((*newIt).name() < (*it).name()) { beginInsertRows({}, row, row); it = m_products.insert(it, (*newIt)); endInsertRows(); ++it; ++newIt; } else if ((*it).name() < (*newIt).name()) { beginRemoveRows({}, row, row); it = m_products.erase(it); endRemoveRows(); } else { *it = *newIt; emit dataChanged(index(row, 0), index(row, 0)); ++it; ++newIt; } } if (it == m_products.end() && newIt != products.cend()) { // trailing insert const auto count = std::distance(newIt, products.cend()); beginInsertRows({}, m_products.size(), m_products.size() + count - 1); while (newIt != products.cend()) m_products.push_back(*newIt++); endInsertRows(); } else if (newIt == products.cend() && it != m_products.end()) { // trailing remove const auto start = std::distance(m_products.begin(), it); const auto end = m_products.size() - 1; beginRemoveRows({}, start, end); m_products.resize(start); endResetModel(); } } diff --git a/src/console/model/surveymodel.cpp b/src/console/model/surveymodel.cpp index a98d1dd..4da1644 100644 --- a/src/console/model/surveymodel.cpp +++ b/src/console/model/surveymodel.cpp @@ -1,134 +1,136 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "surveymodel.h" #include #include #include #include #include using namespace KUserFeedback::Console; SurveyModel::SurveyModel(QObject *parent) : QAbstractTableModel(parent) { } SurveyModel::~SurveyModel() = default; void SurveyModel::setRESTClient(RESTClient* client) { Q_ASSERT(client); m_restClient = client; connect(client, &RESTClient::clientConnected, this, &SurveyModel::reload); reload(); } void SurveyModel::setProduct(const Product& product) { m_product = product; reload(); } void SurveyModel::reload() { if (!m_restClient || !m_restClient->isConnected() || !m_product.isValid()) return; auto reply = RESTApi::listSurveys(m_restClient, m_product); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { beginResetModel(); const auto data = reply->readAll(); m_surveys = Survey::fromJson(data); endResetModel(); } + reply->deleteLater(); }); } int SurveyModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 4; } int SurveyModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_surveys.size(); } QVariant SurveyModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; if (role == Qt::DisplayRole) { const auto survey = m_surveys.at(index.row()); switch (index.column()) { case 0: return survey.name(); case 1: return survey.url().toString(); case 3: return survey.target(); } } else if (role == Qt::CheckStateRole) { if (index.column() == 2) return m_surveys.at(index.row()).isActive() ? Qt::Checked : Qt::Unchecked; } else if (role == SurveyRole) { return QVariant::fromValue(m_surveys.at(index.row())); } return {}; } bool SurveyModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.column() == 2 && role == Qt::CheckStateRole) { auto &survey = m_surveys[index.row()]; survey.setActive(value.toInt() == Qt::Checked); auto reply = RESTApi::updateSurvey(m_restClient, survey); connect(reply, &QNetworkReply::finished, this, [this, reply]() { qDebug() << reply->readAll(); reload(); }); emit dataChanged(index, index); + reply->deleteLater(); return true; } return false; } QVariant SurveyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case 0: return tr("Name"); case 1: return tr("URL"); case 2: return tr("Active"); case 3: return tr("Target"); } } return QAbstractTableModel::headerData(section, orientation, role); } Qt::ItemFlags SurveyModel::flags(const QModelIndex &index) const { auto f = QAbstractTableModel::flags(index); if (index.column() == 2) return f | Qt::ItemIsUserCheckable; return f; } diff --git a/src/console/schemaeditor/schemaeditor.cpp b/src/console/schemaeditor/schemaeditor.cpp index 939b8cd..2722a7a 100644 --- a/src/console/schemaeditor/schemaeditor.cpp +++ b/src/console/schemaeditor/schemaeditor.cpp @@ -1,181 +1,182 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "schemaeditor.h" #include "ui_schemaeditor.h" #include #include #include #include #include #include #include #include using namespace KUserFeedback::Console; SchemaEditor::SchemaEditor(QWidget* parent) : QWidget(parent), ui(new Ui::SchemaEditor) { ui->setupUi(this); connect(ui->schema, &SchemaEditWidget::logMessage, this, &SchemaEditor::logMessage); connect(ui->schema, &SchemaEditWidget::productChanged, ui->aggregation, [this]() { ui->aggregation->setProduct(product()); setDirty(); }); connect(ui->aggregation, &AggregationEditWidget::productChanged, this, [this]() { setDirty(); }); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &SchemaEditor::updateState); auto templateMenu = new QMenu(tr("Source Templates"), this); for (const auto &t : SchemaEntryTemplates::availableTemplates()) { auto a = templateMenu->addAction(t.name()); a->setData(QVariant::fromValue(t)); connect(a, &QAction::triggered, this, [this, a]() { const auto t = a->data().value(); auto p = product(); p.addTemplate(t); setProduct(p); setDirty(); }); } m_createFromTemplateAction = templateMenu->menuAction(); m_createFromTemplateAction->setIcon(QIcon::fromTheme(QStringLiteral("document-new-from-template"))); ui->actionSave->setShortcut(QKeySequence::Save); connect(ui->actionSave, &QAction::triggered, this, &SchemaEditor::save); connect(ui->actionImportSchema, &QAction::triggered, this, &SchemaEditor::importSchema); connect(ui->actionExportSchema, &QAction::triggered, this, &SchemaEditor::exportSchema); addActions({ m_createFromTemplateAction, ui->actionSave, ui->actionImportSchema, ui->actionExportSchema }); auto sep = new QAction(this); sep->setSeparator(true); addAction(sep); addActions(ui->schema->actions()); addActions(ui->aggregation->actions()); updateState(); } SchemaEditor::~SchemaEditor() = default; void SchemaEditor::setRESTClient(RESTClient* client) { m_restClient = client; ui->schema->setRESTClient(client); } Product SchemaEditor::product() const { auto p = ui->schema->product(); p.setAggregations(ui->aggregation->product().aggregations()); return p; } void SchemaEditor::setProduct(const Product& product) { ui->schema->setProduct(product); ui->aggregation->setProduct(product); setDirty(false); } bool SchemaEditor::isDirty() const { return m_isDirty; } void SchemaEditor::setDirty(bool dirty) { m_isDirty = dirty; updateState(); } void SchemaEditor::save() { auto reply = RESTApi::updateProduct(m_restClient, product()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) return; setDirty(false); emit logMessage(QString::fromUtf8((reply->readAll()))); emit productChanged(product()); + reply->deleteLater(); }); } void SchemaEditor::exportSchema() { const auto fileName = QFileDialog::getSaveFileName(this, tr("Export Schema")); if (fileName.isEmpty()) return; QFile f(fileName); if (!f.open(QFile::WriteOnly)) { QMessageBox::critical(this, tr("Export Failed"), tr("Could not open file: %1").arg(f.errorString())); return; } f.write(product().toJson()); emit logMessage(tr("Schema of %1 exported to %2.").arg(product().name(), f.fileName())); } void SchemaEditor::importSchema() { const auto fileName = QFileDialog::getOpenFileName(this, tr("Import Schema")); if (fileName.isEmpty()) return; QFile f(fileName); if (!f.open(QFile::ReadOnly)) { QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file: %1").arg(f.errorString())); return; } const auto products = Product::fromJson(f.readAll()); if (products.size() != 1 || !products.at(0).isValid()) { QMessageBox::critical(this, tr("Import Failed"), tr("Selected file contains no valid product schema.")); return; } auto p = products.at(0); p.setName(product().name()); setProduct(p); setDirty(); emit logMessage(tr("Schema of %1 imported from %2.").arg(product().name(), f.fileName())); } void SchemaEditor::updateState() { const auto p = product(); m_createFromTemplateAction->setEnabled(p.isValid()); ui->actionSave->setEnabled(p.isValid() && isDirty()); ui->actionExportSchema->setEnabled(p.isValid()); ui->actionImportSchema->setEnabled(p.isValid()); const auto schemaEditActive = ui->tabWidget->currentWidget() == ui->schema; const auto aggrEditActive = ui->tabWidget->currentWidget() == ui->aggregation; for (auto action : ui->schema->actions()) action->setVisible(schemaEditActive); for (auto action : ui->aggregation->actions()) action->setVisible(aggrEditActive); } diff --git a/src/console/surveyeditor/surveyeditor.cpp b/src/console/surveyeditor/surveyeditor.cpp index 2e609bd..3ccaae1 100644 --- a/src/console/surveyeditor/surveyeditor.cpp +++ b/src/console/surveyeditor/surveyeditor.cpp @@ -1,138 +1,141 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "surveyeditor.h" #include "ui_surveyeditor.h" #include "surveydialog.h" #include #include #include #include #include using namespace KUserFeedback::Console; SurveyEditor::SurveyEditor(QWidget* parent) : QWidget(parent), ui(new Ui::SurveyEditor), m_surveyModel(new SurveyModel(this)) { ui->setupUi(this); ui->surveyView->setModel(m_surveyModel); ui->surveyView->addActions({ ui->actionAddSurvey, ui->actionEditSurvey, ui->actionDeleteSurvey }); connect(ui->surveyView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SurveyEditor::updateActions); connect(ui->surveyView, &QAbstractItemView::doubleClicked, this, &SurveyEditor::editSurvey); connect(m_surveyModel, &QAbstractItemModel::modelReset, this, &SurveyEditor::updateActions); connect(ui->actionAddSurvey, &QAction::triggered, this, &SurveyEditor::createSurvey); connect(ui->actionEditSurvey, &QAction::triggered, this, &SurveyEditor::editSurvey); connect(ui->actionDeleteSurvey, &QAction::triggered, this, &SurveyEditor::deleteSurvey); addActions({ ui->actionAddSurvey, ui->actionEditSurvey, ui->actionDeleteSurvey }); updateActions(); } SurveyEditor::~SurveyEditor() = default; void SurveyEditor::setRESTClient(RESTClient* client) { m_restClient = client; m_surveyModel->setRESTClient(client); } void SurveyEditor::setProduct(const Product& product) { m_product = product; m_surveyModel->setProduct(product); updateActions(); } void SurveyEditor::createSurvey() { if (!m_product.isValid()) return; SurveyDialog dlg(this); if (!dlg.exec()) return; auto reply = RESTApi::createSurvey(m_restClient, m_product, dlg.survey()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NoError) { emit logMessage(QString::fromUtf8(reply->readAll())); } m_surveyModel->reload(); + reply->deleteLater(); }); } void SurveyEditor::editSurvey() { if (!m_product.isValid()) return; const auto selection = ui->surveyView->selectionModel()->selectedRows(); if (selection.isEmpty()) return; const auto survey = selection.first().data(SurveyModel::SurveyRole).value(); SurveyDialog dlg; dlg.setSurvey(survey); if (dlg.exec() != QDialog::Accepted) return; auto reply = RESTApi::updateSurvey(m_restClient, dlg.survey()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) return; emit logMessage(QString::fromUtf8(reply->readAll())); m_surveyModel->reload(); + reply->deleteLater(); }); } void SurveyEditor::deleteSurvey() { if (!m_product.isValid()) return; const auto selection = ui->surveyView->selectionModel()->selectedRows(); if (selection.isEmpty()) return; const auto r = QMessageBox::critical(this, tr("Delete Survey"), tr("Do you really want to delete the selected survey?"), QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); if (r != QMessageBox::Discard) return; const auto survey = selection.first().data(SurveyModel::SurveyRole).value(); if (survey.uuid().isNull()) return; auto reply = RESTApi::deleteSurvey(m_restClient, survey); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) return; emit logMessage(QString::fromUtf8(reply->readAll())); m_surveyModel->reload(); + reply->deleteLater(); }); } void SurveyEditor::updateActions() { ui->actionAddSurvey->setEnabled(m_product.isValid()); const auto hasSelection = !ui->surveyView->selectionModel()->selectedRows().isEmpty(); ui->actionEditSurvey->setEnabled(hasSelection); ui->actionDeleteSurvey->setEnabled(hasSelection); }