Commit 48764e54 authored by Volker Krause's avatar Volker Krause
Browse files

Start to add surveys.

Listing works, creating partially works, update/delete still missing.
parent 6d5ed3b7
......@@ -7,6 +7,8 @@ set(analyzer_srcs
productmodel.cpp
restclient.cpp
serverinfo.cpp
survey.cpp
surveymodel.cpp
)
add_executable(UserFeedbackAnalyzer ${analyzer_srcs})
......
......@@ -23,6 +23,7 @@
#include "productmodel.h"
#include "restclient.h"
#include "serverinfo.h"
#include "surveymodel.h"
#include <QApplication>
#include <QDebug>
......@@ -42,15 +43,18 @@ MainWindow::MainWindow() :
ui(new Ui::MainWindow),
m_restClient(new RESTClient(this)),
m_productModel(new ProductModel(this)),
m_dataModel(new DataModel(this))
m_dataModel(new DataModel(this)),
m_surveyModel(new SurveyModel(this))
{
ui->setupUi(this);
ui->productListView->setModel(m_productModel);
ui->dataView->setModel(m_dataModel);
ui->surveyView->setModel(m_surveyModel);
setWindowIcon(QIcon::fromTheme(QStringLiteral("search")));
m_productModel->setRESTClient(m_restClient);
m_dataModel->setRESTClient(m_restClient);
m_surveyModel->setRESTClient(m_restClient);
ui->actionConnectToServer->setIcon(QIcon::fromTheme(QStringLiteral("network-connect")));
connect(ui->actionConnectToServer, &QAction::triggered, this, [this]() {
......@@ -84,7 +88,7 @@ MainWindow::MainWindow() :
});
});
ui->actionDeleteProduct->setIcon(QIcon::fromTheme(QStringLiteral("folder-delete")));
ui->actionDeleteProduct->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
connect(ui->actionDeleteProduct, &QAction::triggered, this, [this]() {
auto sel = ui->productListView->selectionModel()->selectedRows();
if (sel.isEmpty())
......@@ -102,16 +106,46 @@ MainWindow::MainWindow() :
});
});
ui->actionAddSurvey->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
connect(ui->actionAddSurvey, &QAction::triggered, this, [this]() {
const auto product = selectedProduct();
if (product.isEmpty())
return;
const auto surveyUrl = QInputDialog::getText(this, tr("Add New Survey"), tr("Survey URL:"));
if (surveyUrl.isEmpty())
return;
Survey survey;
survey.setName(surveyUrl); // TODO
survey.setUrl(QUrl(surveyUrl));
auto reply = m_restClient->post(QStringLiteral("surveys/") + product, survey.toJson());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
logMessage(QString::fromUtf8(reply->readAll()));
} else {
logError(reply->errorString());
}
m_surveyModel->reload();
});
});
ui->actionDeleteSurvey->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
connect(ui->actionDeleteSurvey, &QAction::triggered, this, [this]() {
const auto product = selectedProduct();
if (product.isEmpty())
return;
// TODO
});
ui->actionQuit->setShortcut(QKeySequence::Quit);
ui->actionQuit->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
connect(ui->actionQuit, &QAction::triggered, QCoreApplication::instance(), &QCoreApplication::quit);
connect(ui->productListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selection) {
if (selection.isEmpty())
connect(ui->productListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this]() {
const auto product = selectedProduct();
if (product.isEmpty())
return;
const auto idx = selection.first().topLeft();
const auto product = idx.data();
m_dataModel->setProductId(product.toString());
m_dataModel->setProductId(product);
m_surveyModel->setProductId(product);
});
QTimer::singleShot(0, ui->actionConnectToServer, &QAction::trigger);
......@@ -144,3 +178,12 @@ void MainWindow::logError(const QString& msg)
ui->logWidget->append(msg);
ui->logWidget->append(QStringLiteral("</font>"));
}
QString MainWindow::selectedProduct() const
{
const auto selection = ui->productListView->selectionModel()->selectedRows();
if (selection.isEmpty())
return {};
const auto idx = selection.first();
return idx.data().toString();
}
......@@ -36,6 +36,7 @@ class DataModel;
class ProductModel;
class ServerInfo;
class RESTClient;
class SurveyModel;
class MainWindow : public QMainWindow
{
......@@ -49,10 +50,13 @@ private:
void logMessage(const QString &msg);
void logError(const QString &msg);
QString selectedProduct() const;
std::unique_ptr<Ui::MainWindow> ui;
RESTClient *m_restClient;
ProductModel *m_productModel;
DataModel *m_dataModel;
SurveyModel *m_surveyModel;
};
}
}
......
......@@ -15,8 +15,51 @@
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="dataView"/>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="dataTab">
<attribute name="title">
<string>Raw Data</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QTableView" name="dataView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="surveyTab">
<attribute name="title">
<string>Surveys</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QTreeView" name="surveyView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
......@@ -44,8 +87,16 @@
<addaction name="actionAddProduct"/>
<addaction name="actionDeleteProduct"/>
</widget>
<widget class="QMenu" name="menu_Survery">
<property name="title">
<string>&amp;Survery</string>
</property>
<addaction name="actionAddSurvey"/>
<addaction name="actionDeleteSurvey"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menu_Products"/>
<addaction name="menu_Survery"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="logDock">
......@@ -123,6 +174,22 @@
<string>Delete product and all associated data.</string>
</property>
</action>
<action name="actionAddSurvey">
<property name="text">
<string>&amp;Add Survey...</string>
</property>
<property name="toolTip">
<string>Add new survery.</string>
</property>
</action>
<action name="actionDeleteSurvey">
<property name="text">
<string>&amp;Delete Survey</string>
</property>
<property name="toolTip">
<string>Delete a survey.</string>
</property>
</action>
</widget>
<resources/>
<connections/>
......
/*
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#include "survey.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSharedData>
#include <QString>
#include <QUrl>
using namespace UserFeedback::Analyzer;
namespace UserFeedback {
namespace Analyzer {
class SurveyData : public QSharedData
{
public:
QString name;
QUrl url;
};
}
}
Survey::Survey() : d(new SurveyData) {}
Survey::Survey(const Survey&) = default;
Survey::~Survey() = default;
Survey& Survey::operator=(const Survey&) = default;
QString Survey::name() const
{
return d->name;
}
void Survey::setName(const QString& name)
{
d->name = name;
}
QUrl Survey::url() const
{
return d->url;
}
void Survey::setUrl(const QUrl& url)
{
d->url = url;
}
QByteArray Survey::toJson() const
{
QJsonObject obj;
obj.insert(QStringLiteral("name"), name());
obj.insert(QStringLiteral("url"), url().toString());
return QJsonDocument(obj).toJson();
}
QVector<Survey> Survey::fromJson(const QByteArray &data)
{
QVector<Survey> surveys;
foreach (const auto &v, QJsonDocument::fromJson(data).array()) {
const auto obj = v.toObject();
Survey survey;
survey.setName(obj.value(QStringLiteral("name")).toString());
survey.setUrl(QUrl(obj.value(QStringLiteral("url")).toString()));
surveys.push_back(survey);
}
return surveys;
}
/*
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef USERFEEDBACK_ANALYZER_SURVEY_H
#define USERFEEDBACK_ANALYZER_SURVEY_H
#include <QSharedDataPointer>
#include <QVector>
class QString;
class QUrl;
namespace UserFeedback {
namespace Analyzer {
class SurveyData;
/** Data for one survey. */
class Survey
{
public:
Survey();
Survey(const Survey&);
~Survey();
Survey& operator=(const Survey&);
QString name() const;
void setName(const QString& name);
QUrl url() const;
void setUrl(const QUrl& url);
QByteArray toJson() const;
static QVector<Survey> fromJson(const QByteArray &data);
private:
QSharedDataPointer<SurveyData> d;
};
}
}
Q_DECLARE_TYPEINFO(UserFeedback::Analyzer::Survey, Q_MOVABLE_TYPE);
#endif // USERFEEDBACK_ANALYZER_SURVEY_H
/*
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#include "surveymodel.h"
#include "restclient.h"
#include <QDebug>
#include <QNetworkReply>
#include <QUrl>
using namespace UserFeedback::Analyzer;
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::setProductId(const QString& product)
{
m_productId = product;
reload();
}
void SurveyModel::reload()
{
if (!m_restClient || !m_restClient->isConnected() || m_productId.isEmpty())
return;
auto reply = m_restClient->get(QStringLiteral("surveys/") + m_productId);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
beginResetModel();
const auto data = reply->readAll();
m_surveys = Survey::fromJson(data);
endResetModel();
} else {
qWarning() << reply->errorString();
}
});
}
int SurveyModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 2;
}
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();
}
}
return {};
}
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");
}
}
return QAbstractTableModel::headerData(section, orientation, role);
}
/*
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef USERFEEDBACK_ANALYZER_SURVEYMODEL_H
#define USERFEEDBACK_ANALYZER_SURVEYMODEL_H
#include "survey.h"
#include <QAbstractTableModel>
namespace UserFeedback {
namespace Analyzer {
class RESTClient;
class SurveyModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit SurveyModel(QObject *parent = nullptr);
~SurveyModel();
void setRESTClient(RESTClient *client);
void setProductId(const QString &product);
void reload();
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
private:
RESTClient *m_restClient = nullptr;
QString m_productId;
QVector<Survey> m_surveys;
};
}
}
#endif // USERFEEDBACK_ANALYZER_SURVEYMODEL_H
......@@ -71,6 +71,41 @@ public function get_data($product)
echo(json_encode($data));
}
/** List all surveys for a product. */
public function get_surveys($product)
{
if ($product == "")
die("No product id specified.");
$db = new DataStore();
$res = $db->surveysByProductName($product);
echo(json_encode($res));
}
/** Add new survey. */
public function post_surveys($product)
{
if ($product == "")
die("No product id specified.");
$rawPostData = file_get_contents('php://input');
$survey= json_decode($rawPostData, true);
$db = new DataStore();
$productData = $db->productByName($product);
if (is_null($productData))
die("Invalid product identifier.");
$db->addSurvey($productData['id'], $survey);
echo("Survey created for product $product.");
}
/** Delete survey. */
public function delete_surveys($product, $survey)
{
echo("TODO: DELETE surveys for $product/$survey.");
}
}
?>
......@@ -160,6 +160,29 @@ public function rawDataForProduct($name)
return $data;
}
/** List all survey for a product name. */
public function surveysByProductName($product)
{
$res = $this->db->query('SELECT surveys.* FROM surveys JOIN products ON (surveys.productId = products.id) WHERE products.name = ' . $this->db->quote($product));
if ($res === FALSE)
$this->fatalDbError();
$surveys = array();
foreach ($res as $row)
array_push($surveys, $row);
return $surveys;
}
/** Add a new survey for a product given by id. */
public function addSurvey($productId, $survey)
{
$res = $this->db->exec('INSERT INTO surveys (productId, name, url) VALUES ('
. $productId . ', '
. $this->db->quote($survey['name']) . ', '
. $this->db->quote($survey['url']) . ')');
if ($res === FALSE)
$this->fatalDbError();
}
}
?>
......@@ -23,7 +23,7 @@ class RESTDispatcher
public static function dispatch($handler)
{
$prefix = dirname($_SERVER['PHP_SELF']);
$command = explode('/', substr($_SERVER['REQUEST_URI'], strlen($prefix) + 1), 2);
$command = explode('/', substr($_SERVER['REQUEST_URI'], strlen($prefix) + 1), 3);
if (sizeof($command) < 1)
die('Empty REST command.');
......@@ -32,10 +32,17 @@ public static function dispatch($handler)
if (!method_exists($handler, $method))
die('Invalid REST command ' . $method . '.');
if (sizeof($command) == 1)
$handler->$method();
else
$handler->$method($command[1]);