Commit 6d5ed3b7 authored by Volker Krause's avatar Volker Krause
Browse files

Start of a usage statistics and user engagement framework.

So far we can track basic usage statistics and report them to the server,
create and delete products on the server, and look at the raw data using
the analyzer tool.
parents
*.kdev4
*~
*.rej
*.orig
*.out
CMakeLists.txt.user
// kate: space-indent on; indent-width 4; remove-trailing-space on; remove-trailing-space-save on;
project(UserFeedback)
cmake_minimum_required(VERSION 2.8.12)
if(POLICY CMP0063)
cmake_policy(SET CMP0063 NEW)
endif()
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ ${CMAKE_MODULE_PATH})
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
if(NOT DEFINED CMAKE_INSTALL_RPATH_USE_LINK_PATH)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()
if(NOT DEFINED CMAKE_MACOSX_RPATH)
set(CMAKE_MACOSX_RPATH TRUE)
endif()
enable_testing()
set(USERFEEDBACK_VERSION_MAJOR "1")
set(USERFEEDBACK_VERSION_MINOR "9")
set(USERFEEDBACK_VERSION_PATCH "84")
set(USERFEEDBACK_VERSION "${USERFEEDBACK_VERSION_MAJOR}.${USERFEEDBACK_VERSION_MINOR}.${USERFEEDBACK_VERSION_PATCH}")
set(PROJECT_VERSION_STRING ${USERFEEDBACK_VERSION}) # for the ECM pri generator
include(ECMGeneratePriFile)
include(ECMEnableSanitizers)
include(FeatureSummary)
include(GenerateExportHeader)
#
# Compiler & linker settings
#
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
if(CMAKE_COMPILER_IS_GNUCXX OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated -Wextra -Wall -Werror=return-type -std=c++0x")
endif()
if(NOT ECM_ENABLE_SANITIZERS AND (CMAKE_SYSTEM_NAME MATCHES Linux OR CMAKE_SYSTEM_NAME STREQUAL GNU))
if(CMAKE_COMPILER_IS_GNUCXX OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--fatal-warnings -Wl,--no-undefined -lc ${CMAKE_SHARED_LINKER_FLAGS}")
set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--fatal-warnings -Wl,--no-undefined -lc ${CMAKE_MODULE_LINKER_FLAGS}")
endif()
endif()
#
# Dependencies
#
# try Qt5 first, and prefer that (if found), but only if not disabled via option
if(NOT ENFORCE_QT4_BUILD)
find_package(Qt5Core QUIET NO_MODULE)
else()
set(Qt5Core_FOUND FALSE)
endif()
if(Qt5Core_FOUND)
set_package_properties(Qt5Core PROPERTIES TYPE REQUIRED)
find_package(Qt5 NO_MODULE REQUIRED COMPONENTS Network)
find_package(Qt5 NO_MODULE QUIET OPTIONAL_COMPONENTS Widgets)
if(Qt5_POSITION_INDEPENDENT_CODE AND NOT WIN32)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
include("cmake/ECMQt4To5Porting.cmake")
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x040800)
add_definitions(-DQT_DEPRECATED_WARNINGS)
set_package_properties(Qt5 PROPERTIES URL "http://qt-project.org/")
set_package_properties(Qt5Widgets PROPERTIES TYPE RECOMMENDED PURPOSE "Required for analyzer and administraion tool.")
# Qt4
else()
set(QT_USE_IMPORTED_TARGETS true)
find_package(Qt4 4.8.0 REQUIRED QtCore QtNetwork QtGui)
include(${QT_USE_FILE})
set_package_properties(Qt4 PROPERTIES URL "http://qt-project.org/")
# C++11/Qt5 compatibility
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FI\"${CMAKE_SOURCE_DIR}\\compat\\qt4compat.h\"")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include \"${CMAKE_SOURCE_DIR}/compat/qt4compat.h\"")
endif()
endif()
# debug suffixes for qmake compatibility
if(WIN32)
set(CMAKE_DEBUG_POSTFIX "d")
elseif(APPLE)
set(CMAKE_DEBUG_POSTFIX "_debug")
endif()
add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_ASCII)
#
# Installation settings
#
set(BIN_INSTALL_DIR "bin")
set(LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)")
set(LIB_INSTALL_DIR "lib${LIB_SUFFIX}")
set(INCLUDE_INSTALL_DIR "include/userfeedback")
set(CMAKECONFIG_INSTALL_DIR ${LIB_INSTALL_DIR}/cmake/UserFeedback)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${BIN_INSTALL_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${LIB_INSTALL_DIR})
# set RPATH only when installing to a non-default location and not set in cache
if(NOT CMAKE_INSTALL_RPATH)
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}" _isSystemPlatformLibDir)
list(FIND CMAKE_C_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}" _isSystemCLibDir)
list(FIND CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}" _isSystemCxxLibDir)
if(${_isSystemPlatformLibDir} EQUAL -1 AND ${_isSystemCLibDir} EQUAL -1 AND ${_isSystemCxxLibDir} EQUAL -1)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}")
endif()
endif()
set(
INSTALL_TARGETS_DEFAULT_ARGS
RUNTIME DESTINATION ${BIN_INSTALL_DIR}
LIBRARY DESTINATION ${LIB_INSTALL_DIR}
ARCHIVE DESTINATION ${LIB_INSTALL_DIR} COMPONENT Devel
BUNDLE DESTINATION ${BUNDLE_INSTALL_DIR}
)
#
# Actually build the stuff
#
add_subdirectory(provider)
if(Qt5Widgets_FOUND AND NOT CMAKE_VERSION VERSION_LESS 3.0) # analyzer is Qt5 only and needs AUTOUIC support
add_subdirectory(analyzer)
endif()
add_subdirectory(server)
add_subdirectory(tests/manual)
#
# CMake package config file generation
#
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/UserFeedbackConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/UserFeedbackConfig.cmake
INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}
PATH_VARS INCLUDE_INSTALL_DIR
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/UserFeedbackConfigVersion.cmake
VERSION ${USERFEEDBACK_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/UserFeedbackConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/UserFeedbackConfigVersion.cmake
DESTINATION ${CMAKECONFIG_INSTALL_DIR}
)
install(EXPORT UserFeedbackTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE UserFeedbackTarget.cmake)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
@PACKAGE_INIT@
if(@Qt5Core_FOUND@)
find_package(Qt5 @Qt5Core_VERSION_MAJOR@.@Qt5Core_VERSION_MINOR@ NO_MODULE REQUIRED COMPONENTS Core Network)
find_package(Qt5 @Qt5Core_VERSION_MAJOR@.@Qt5Core_VERSION_MINOR@ NO_MODULE COMPONENTS Widgets)
else()
find_package(Qt4 @QT_VERSION_MAJOR@.@QT_VERSION_MINOR@)
endif()
include("${CMAKE_CURRENT_LIST_DIR}/UserFeedbackTarget.cmake")
#!/bin/bash
find "$@" -name '*.h' -o -name '*.cpp' -o -name '*.qml' -o -name '*.c' | grep -v /3rdparty/ | grep -v /build | while read FILE; do
if grep -qiE "Copyright \(C\) [0-9, -]{4,} " "$FILE" ; then continue; fi
thisfile=`basename $FILE`
authorName=`git config user.name`
authorEmail=`git config user.email`
thisYear=`date +%Y`
cat <<EOF > "$FILE".tmp
/*
Copyright (C) $thisYear $authorName <$authorEmail>
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/>.
*/
EOF
cat "$FILE" >> "$FILE".tmp
mv "$FILE".tmp "$FILE"
done
set(analyzer_srcs
connectdialog.cpp
datamodel.cpp
main.cpp
mainwindow.cpp
product.cpp
productmodel.cpp
restclient.cpp
serverinfo.cpp
)
add_executable(UserFeedbackAnalyzer ${analyzer_srcs})
target_link_libraries(UserFeedbackAnalyzer Qt5::Widgets Qt5::Network)
install(TARGETS UserFeedbackAnalyzer ${INSTALL_TARGETS_DEFAULT_ARGS})
/*
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 "connectdialog.h"
#include "ui_connectdialog.h"
#include "serverinfo.h"
#include <QIcon>
#include <QUrl>
using namespace UserFeedback::Analyzer;
ConnectDialog::ConnectDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ConnectDialog)
{
ui->setupUi(this);
setWindowIcon(QIcon::fromTheme(QStringLiteral("network-connect")));
}
ConnectDialog::~ConnectDialog()
{
}
ServerInfo ConnectDialog::serverInfo() const
{
ServerInfo info;
info.setUrl(QUrl(ui->serverUrl->text()));
info.setUserName(ui->userName->text());
info.setPassword(ui->password->text());
return info;
}
void ConnectDialog::setServerInfo(const ServerInfo& serverInfo)
{
ui->serverUrl->setText(serverInfo.url().toString());
ui->userName->setText(serverInfo.userName());
ui->password->setText(serverInfo.password());
}
/*
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_CONNECTDIALOG_H
#define USERFEEDBACK_ANALYZER_CONNECTDIALOG_H
#include <QDialog>
#include <memory>
namespace UserFeedback {
namespace Analyzer {
namespace Ui
{
class ConnectDialog;
}
class ServerInfo;
class ConnectDialog : public QDialog
{
Q_OBJECT
public:
explicit ConnectDialog(QWidget *parent = nullptr);
~ConnectDialog();
ServerInfo serverInfo() const;
void setServerInfo(const ServerInfo &serverInfo);
private:
std::unique_ptr<Ui::ConnectDialog> ui;
};
}
}
#endif // USERFEEDBACK_ANALYZER_CONNECTDIALOG_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UserFeedback::Analyzer::ConnectDialog</class>
<widget class="QDialog" name="UserFeedback::Analyzer::ConnectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>387</width>
<height>298</height>
</rect>
</property>
<property name="windowTitle">
<string>Connect To Analytics Server</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Se&amp;rver URL:</string>
</property>
<property name="buddy">
<cstring>serverUrl</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="serverUrl"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;User name:</string>
</property>
<property name="buddy">
<cstring>userName</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="userName"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>P&amp;assword:</string>
</property>
<property name="buddy">
<cstring>password</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>UserFeedback::Analyzer::ConnectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>UserFeedback::Analyzer::ConnectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
/*
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 "datamodel.h"
#include "restclient.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
using namespace UserFeedback::Analyzer;
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();
}
void DataModel::setProductId(const QString& productId)
{
m_productId = productId;
reload();
}
void UserFeedback::Analyzer::DataModel::reload()
{
if (!m_restClient || !m_restClient->isConnected() || m_productId.isEmpty())
return;
auto reply = m_restClient->get(QStringLiteral("data/") + m_productId);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
beginResetModel();
m_data = QJsonDocument::fromJson(reply->readAll()).array();
endResetModel();
} else {
qDebug() << Q_FUNC_INFO << reply->errorString();
}
});
}
int DataModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 5;
}
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 obj = m_data.at(index.row()).toObject();
switch (index.column()) {
case 0: return obj.value(QStringLiteral("timestamp")).toString();
case 1: return obj.value(QStringLiteral("version")).toString();
case 2: return obj.value(QStringLiteral("platform")).toString();
case 3: return obj.value(QStringLiteral("startCount")).toString();
case 4: return obj.value(QStringLiteral("usageTime")).toString();
}
}
return {};
}
QVariant DataModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch(section) {
case 0: return tr("Timestamp");
case 1: return tr("Version");
case 2: return tr("Platform");
case 3: return tr("Starts");
case 4: return tr("Used Time");
}
}
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_DATAMODEL_H
#define USERFEEDBACK_ANALYZER_DATAMODEL_H
#include <QAbstractTableModel>
#include <QJsonArray>
namespace UserFeedback {
namespace Analyzer {
class RESTClient;
/** Raw data model showing reported data of a product. */
class DataModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit DataModel(QObject *parent = nullptr);
~DataModel();
void setRESTClient(RESTClient *client);
void setProductId(const QString &productId);
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:
QString m_productId;
RESTClient *m_restClient = nullptr;