diff --git a/.clang-tidy b/.clang-tidy index fd21b3531..543d5ca53 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -40,6 +40,7 @@ Checks: -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-special-member-functions, + -cppcoreguidelines-use-enum-class, -hicpp-avoid-c-arrays, -hicpp-avoid-goto, -hicpp-braces-around-statements, diff --git a/.github/actions/appimage/action.yml b/.github/actions/appimage/action.yml index 91f54a335..808c9ca1e 100644 --- a/.github/actions/appimage/action.yml +++ b/.github/actions/appimage/action.yml @@ -39,6 +39,7 @@ runs: rm ${{ env.QT_ROOT_DIR }}/plugins/sqldrivers/libqsqlmimer.so rm ${{ env.QT_ROOT_DIR }}/plugins/sqldrivers/libqsqlmysql.so rm ${{ env.QT_ROOT_DIR }}/plugins/sqldrivers/libqsqlodbc.so + rm ${{ env.QT_ROOT_DIR }}/plugins/sqldrivers/libqsqloci.so shell: bash - name: Create AppImage diff --git a/.github/actions/cmake/action.yml b/.github/actions/cmake/action.yml index 4ff81aa9e..ac208298e 100644 --- a/.github/actions/cmake/action.yml +++ b/.github/actions/cmake/action.yml @@ -66,6 +66,7 @@ runs: -B '${{github.workspace}}/build' \ -DCMAKE_BUILD_TYPE=Release \ -DQF_BUILD_QML_PLUGINS=ON \ + -DQF_WITH_LIBSHV=ON \ -DBUILD_TESTING=OFF \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DUSE_QT6=${{ inputs.use_qt6 }} \ diff --git a/.github/actions/run-linter/action.yml b/.github/actions/run-linter/action.yml index c7e0771a9..3768f8833 100644 --- a/.github/actions/run-linter/action.yml +++ b/.github/actions/run-linter/action.yml @@ -11,7 +11,7 @@ runs: - name: Setup CMake uses: ./.github/actions/cmake with: - qt_version: 6.8.3 + qt_version: 6.10.1 use_qt6: ON modules: qtserialport qtmultimedia additional_cmake_args: -DCMAKE_GLOBAL_AUTOGEN_TARGET=ON -DCMAKE_AUTOGEN_ORIGIN_DEPENDS=OFF @@ -20,7 +20,7 @@ runs: - uses: mjp41/workaround8649@c8550b715ccdc17f89c8d5c28d7a48eeff9c94a8 if: runner.os == 'Linux' with: - os: ubuntu-latest + os: ubuntu-24.04 - name: Build autogenerated stuff shell: bash diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index c4fba4011..6b4639fb4 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -12,7 +12,7 @@ on: jobs: ubuntu-qe3: - name: Qt 6.8 / Ubuntu 22.04 + name: Qt 6.10 / Ubuntu 22.04 runs-on: ubuntu-22.04 steps: - name: Clone the repository @@ -23,7 +23,7 @@ jobs: - name: Setup CMake uses: ./.github/actions/cmake with: - qt_version: 6.8.3 + qt_version: 6.10.1 use_qt6: ON modules: qtserialport qtmultimedia additional_cmake_args: -DCMAKE_INSTALL_PREFIX='${{ github.workspace }}/install/usr' diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index b4c69395c..2ed6fbf86 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -8,7 +8,7 @@ on: jobs: windows: - name: Qt 6.8 / Windows + name: Qt 6.10 / Windows runs-on: windows-2025 steps: - name: Clone the repository @@ -62,7 +62,7 @@ jobs: - name: Setup CMake uses: ./.github/actions/cmake with: - qt_version: 6.8.3 + qt_version: 6.10.1 qt_arch: win64_mingw use_qt6: ON modules: qtserialport qtmultimedia diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 82fc37b7a..335707572 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,8 +8,8 @@ on: jobs: clang-tidy: - name: clang-tidy / Ubuntu 22.04 - runs-on: ubuntu-22.04 + name: clang-tidy / Ubuntu 24.04 + runs-on: ubuntu-24.04 env: CC: clang CXX: clang++ @@ -25,8 +25,8 @@ jobs: lint_program_with_args: clang-tidy --quiet --warnings-as-errors=* clazy: - name: clazy / Ubuntu 22.04 - runs-on: ubuntu-22.04 + name: clazy / Ubuntu 24.04 + runs-on: ubuntu-24.04 env: CC: clang CXX: clang++ diff --git a/.gitmodules b/.gitmodules index 6e2bec6bb..5006631e0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "3rdparty/necrolog"] path = 3rdparty/necrolog url = https://github.com/fvacek/necrolog.git +[submodule "3rdparty/libshv"] + path = 3rdparty/libshv + url = https://github.com/silicon-heaven/libshv.git diff --git a/3rdparty/libshv b/3rdparty/libshv new file mode 160000 index 000000000..f77c05674 --- /dev/null +++ b/3rdparty/libshv @@ -0,0 +1 @@ +Subproject commit f77c05674413e5bd5a1a98c894385e89805406d0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 53fe104c1..025c808c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.18.4) set(QF_BUILD_QML_PLUGINS ON CACHE BOOL "Build with QML Plugins support") +set(QF_WITH_LIBSHV OFF CACHE BOOL "Build with libshv") project(quickbox LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) @@ -26,6 +27,9 @@ endif (WIN32) if (NOT TARGET libnecrolog) add_subdirectory(3rdparty/necrolog) endif() +if (QF_WITH_LIBSHV) + add_subdirectory(3rdparty/libshv) +endif() set(USE_QT6 ON) find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui Sql Qml Xml LinguistTools PrintSupport Svg SerialPort Multimedia Network) diff --git a/libqf/libqfcore/include/qf/core/sql/qxrecchng.h b/libqf/libqfcore/include/qf/core/sql/qxrecchng.h new file mode 100644 index 000000000..7f444660c --- /dev/null +++ b/libqf/libqfcore/include/qf/core/sql/qxrecchng.h @@ -0,0 +1 @@ +#include "../../../../src/sql/qxrecchng.h" diff --git a/libqf/libqfcore/src/sql/qxrecchng.h b/libqf/libqfcore/src/sql/qxrecchng.h new file mode 100644 index 000000000..5ec42a70e --- /dev/null +++ b/libqf/libqfcore/src/sql/qxrecchng.h @@ -0,0 +1,17 @@ +#pragma once + +#include "../core/coreglobal.h" + +namespace qf::core::sql { + +enum class QxRecOp { Insert, Update, Delete, }; + +struct QFCORE_DECL_EXPORT QxRecChng +{ + QString table; + int64_t id; + QVariant record; + QxRecOp op; +}; + +} diff --git a/libqf/libqfcore/src/utils/table.cpp b/libqf/libqfcore/src/utils/table.cpp index 78d073307..c4210506b 100644 --- a/libqf/libqfcore/src/utils/table.cpp +++ b/libqf/libqfcore/src/utils/table.cpp @@ -1310,7 +1310,8 @@ QVariant Table::sumValue(int field_ix) const return ret; } -static void setDomElementText(QDomDocument &owner_doc, QDomElement &el, const QString &str) +namespace { +void setDomElementText(QDomDocument &owner_doc, QDomElement &el, const QString &str) { QDomNode nd = el.firstChild(); QDomText el_txt = nd.toText(); @@ -1322,6 +1323,7 @@ static void setDomElementText(QDomDocument &owner_doc, QDomElement &el, const QS el_txt.setData(str); } } +} QDomElement Table::toHtmlElement(QDomDocument &owner_doc, const QString & col_names, TextExportOptions opts) const { diff --git a/libqf/libqfgui/src/framework/datadialogwidget.h b/libqf/libqfgui/src/framework/datadialogwidget.h index 37c4c1d91..ec4cca8e0 100644 --- a/libqf/libqfgui/src/framework/datadialogwidget.h +++ b/libqf/libqfgui/src/framework/datadialogwidget.h @@ -26,7 +26,7 @@ class QFGUI_DECL_EXPORT DataDialogWidget : public DialogWidget qf::gui::model::DataDocument* dataDocument(bool throw_exc = qf::core::Exception::Throw); - Q_SLOT virtual bool load(const QVariant &id = QVariant(), int mode = qf::gui::model::DataDocument::ModeEdit); + virtual bool load(const QVariant &id = QVariant(), int mode = qf::gui::model::DataDocument::ModeEdit); bool acceptDialogDone(int result) Q_DECL_OVERRIDE; diff --git a/libqf/libqfgui/src/framework/dialogwidget.cpp b/libqf/libqfgui/src/framework/dialogwidget.cpp index f46cd8f23..8d5769997 100644 --- a/libqf/libqfgui/src/framework/dialogwidget.cpp +++ b/libqf/libqfgui/src/framework/dialogwidget.cpp @@ -9,13 +9,13 @@ using namespace qf::gui::framework; -DialogWidget::DialogWidget(QWidget *parent) : - Super(parent), IPersistentSettings(this) +DialogWidget::DialogWidget(QWidget *parent) + : Super(parent) + , IPersistentSettings(this) { } -DialogWidget::~DialogWidget() -= default; +DialogWidget::~DialogWidget() = default; bool DialogWidget::acceptDialogDone(int result) { @@ -23,12 +23,7 @@ bool DialogWidget::acceptDialogDone(int result) Q_UNUSED(result); return true; } -/* -QVariant DialogWidget::acceptDialogDone_qml(const QVariant &result) -{ - return acceptDialogDone(result.toBool()); -} -*/ + void DialogWidget::settleDownInDialog_qml(const QVariant &dlg) { auto *o = dlg.value(); diff --git a/libqf/libqfgui/src/framework/dialogwidget.h b/libqf/libqfgui/src/framework/dialogwidget.h index 13893c8a0..611be6b95 100644 --- a/libqf/libqfgui/src/framework/dialogwidget.h +++ b/libqf/libqfgui/src/framework/dialogwidget.h @@ -32,7 +32,7 @@ class QFGUI_DECL_EXPORT DialogWidget : public Frame, public IPersistentSettings typedef Frame Super; public: explicit DialogWidget(QWidget *parent = nullptr); - ~DialogWidget() Q_DECL_OVERRIDE; + ~DialogWidget() override; QF_PROPERTY_IMPL(QString, t, T, itle) QF_PROPERTY_IMPL(QString, i, I, conSource) diff --git a/libqf/libqfgui/src/framework/ipersistentsettings.cpp b/libqf/libqfgui/src/framework/ipersistentsettings.cpp index c639c374c..7fff48174 100644 --- a/libqf/libqfgui/src/framework/ipersistentsettings.cpp +++ b/libqf/libqfgui/src/framework/ipersistentsettings.cpp @@ -48,7 +48,7 @@ static void callMethodRecursively(QObject *obj, const char *method_name) QMetaMethod mm = obj->metaObject()->method(ix); mm.invoke(obj); } - Q_FOREACH(auto *o, obj->children()) { + for (auto *o : obj->children()) { //static int level = 0; //level++; //QString indent = QString(level, ' '); @@ -103,25 +103,20 @@ QString IPersistentSettings::rawPersistentSettingsPath() QString persistent_id = persistentSettingsId(); QStringList raw_path; if(!persistent_id.isEmpty()) { - for(QObject *obj=m_controlledObject->parent(); obj!=nullptr; obj=obj->parent()) { + for(QObject* obj = m_controlledObject->parent(); obj != nullptr; obj = obj->parent()) { auto *ps = dynamic_cast(obj); if(ps) { QString pp = ps->rawPersistentSettingsPath(); - if(!pp.isEmpty()) + if(!pp.isEmpty()) { raw_path.insert(0, pp); - //qfWarning() << "reading property 'persistentSettingsId' error" << obj << "casted to IPersistentSettings" << ps; - //qfWarning() << "\tcorrect value should be:" << parent_id; + } break; } - QVariant vid = obj->property("persistentSettingsId"); - QString parent_id = vid.toString(); - if(!parent_id.isEmpty()) { - raw_path.insert(0, parent_id); - } - - // reading property using QQmlProperty is crashing my app Qt 5.3.1 commit a83826dad0f62d7a96f5a6093240e4c8f7f2e06e - //QQmlProperty p(obj, "persistentSettingsId"); - //QVariant v2 = p.read(); + QVariant vid = obj->property("persistentSettingsId"); + QString parent_id = vid.toString(); + if(!parent_id.isEmpty()) { + raw_path.insert(0, parent_id); + } } raw_path.append(persistent_id); } diff --git a/libqf/libqfgui/src/model/sqldatadocument.h b/libqf/libqfgui/src/model/sqldatadocument.h index ee56ac5b5..2a2b755e1 100644 --- a/libqf/libqfgui/src/model/sqldatadocument.h +++ b/libqf/libqfgui/src/model/sqldatadocument.h @@ -3,9 +3,7 @@ #include "datadocument.h" #include "sqltablemodel.h" -namespace qf { -namespace gui { -namespace model { +namespace qf::gui::model { class QFGUI_DECL_EXPORT SqlDataDocument : public DataDocument { @@ -21,7 +19,7 @@ class QFGUI_DECL_EXPORT SqlDataDocument : public DataDocument qf::core::sql::QueryBuilder queryBuilder(); void setQueryBuilder(const qf::core::sql::QueryBuilder &qb); protected: - SqlTableModel* createModel(QObject *parent) Q_DECL_OVERRIDE; + SqlTableModel* createModel(QObject *parent) override; ///! load model persistent storage via model bool loadData() Q_DECL_OVERRIDE; @@ -35,5 +33,5 @@ class QFGUI_DECL_EXPORT SqlDataDocument : public DataDocument */ }; -}}} +} diff --git a/libqf/libqfgui/src/model/sqltablemodel.cpp b/libqf/libqfgui/src/model/sqltablemodel.cpp index 276caf493..17ac9ab96 100644 --- a/libqf/libqfgui/src/model/sqltablemodel.cpp +++ b/libqf/libqfgui/src/model/sqltablemodel.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -150,8 +151,7 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) int serial_ix = -1; bool serial_ix_explicitly_set = false; int primary_ix = -1; - //QSqlIndex pri_ix = ti.primaryIndex(); - //bool has_blob_field = false; + QVariantMap qx_record; Q_FOREACH(const qf::core::utils::Table::Field &fld, row_ref.fields()) { i++; if(fld.tableId() != table_id) @@ -186,6 +186,7 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) new_fld.setValue(v); //qfInfo() << "\t\t" << "val is QString:" << (v.metaType().id() == QMetaType::QString); rec.append(new_fld); + qx_record[fld.shortName().toLower()] = v; } } @@ -201,40 +202,23 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) } else { qs = sqldrv->sqlStatement(QSqlDriver::InsertStatement, table, rec, false); - //qs = fixSerialDefaultValue(qs, serial_ix, rec); } - if(qs.isEmpty()) + if(qs.isEmpty()) { continue; - /* - qfDebug() << "\texecuting prepared query:" << qs; - bool ok = q.prepare(qs); - if(!ok) { - qfError() << "Cannot prepare query:" << qs; } - else { - for(int i=0; i= 0 && !serial_ix_explicitly_set) { - QVariant v = q.lastInsertId(); - qfDebug() << "\tsetting serial index:" << serial_ix << "to generated value:" << v; - if(v.isValid()) { - row_ref.setValue(serial_ix, v); + qfDebug() << "\tsetting serial index:" << serial_ix << "to generated value:" << insert_id; + if(insert_id.isValid()) { + row_ref.setValue(serial_ix, insert_id); row_ref.setDirty(serial_ix, false); } else { @@ -251,6 +235,15 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) qfDebug() << "\tsetting value of foreign key" << slave_key << "to value of master key:" << row_ref.value(master_key).toString(); row_ref.setValue(slave_key, row_ref.value(master_key)); } + if (!qx_record.isEmpty() && master_key == "id") { + qf::core::sql::QxRecChng chng { + .table = table_id, + .id = insert_id.toInt(), + .record = qx_record, + .op = qf::core::sql::QxRecOp::Insert, + }; + emit qxRecChng(chng); + } } } else { @@ -268,7 +261,7 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) QSqlDriver *sqldrv = sql_conn.driver(); Q_FOREACH(QString table_id, tableIds(m_table.fields())) { qfDebug() << "\ttableid:" << table_id; - //table = conn.fullTableNameToQtDriverTableName(table); + QVariantMap qx_record; QSqlRecord edit_rec; int i = -1; Q_FOREACH(qfu::Table::Field fld, row_ref.fields()) { @@ -286,12 +279,13 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) //qfDebug() << "\ttableid:" << tableid << "fullTableName:" << fld.fullTableName(); qfDebug() << "\tdirty field" << fld.name() << "type:" << fld.type().id() << "orig val:" << row_ref.origValue(i).toString() << "new val:" << v.toString(); //qfDebug().noSpace() << "\tdirty value: '" << v.toString() << "' isNull(): " << v.isNull() << " type(): " << v.type(); - QSqlField sqlfld(fld.shortName(), fld.type()); + QSqlField sqlfld(fld.shortName().toLower(), fld.type()); sqlfld.setValue(v); //if(sqlfld.type() == QVariant::ByteArray) // has_blob_field = true; qfDebug() << "\tfield is null: " << sqlfld.isNull(); edit_rec.append(sqlfld); + qx_record[fld.shortName()] = v; } if(!edit_rec.isEmpty()) { qfDebug() << "updating table edits:" << table_id; @@ -300,7 +294,9 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) query_str += " "; QSqlRecord where_rec; qfDebug() << "looking for primary index of table:" << table_id; - Q_FOREACH(auto fld_name, sql_conn.primaryIndexFieldNames(table_id)) { + std::optional id_pri_key_value; + auto pri_keys = sql_conn.primaryIndexFieldNames(table_id); + for (const auto &fld_name : pri_keys) { QString full_fld_name = table_id + '.' + fld_name; qfDebug() << "\t checking value of field:" << full_fld_name; int fld_ix = m_table.fields().fieldIndex(full_fld_name); @@ -313,6 +309,9 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) sqlfld.setValue(row_ref.origValue(fld_ix)); qfDebug() << "\tpri index field" << full_fld_name << "type:" << sqlfld.metaType().id() << "orig val:" << row_ref.origValue(fld_ix) << "current val:" << row_ref.value(fld_ix); where_rec.append(sqlfld); + if (auto id = sqlfld.value().toInt(); id > 0) { + id_pri_key_value = id; + } } QF_ASSERT(!where_rec.isEmpty(), QString("pri keys values not generated for table '%1'").arg(table_id), @@ -335,6 +334,15 @@ bool SqlTableModel::postRow(int row_no, bool throw_exc) ret = false; break; } + if (!qx_record.isEmpty() && pri_keys.size() == 1 && pri_keys[0] == "id" && id_pri_key_value.has_value()) { + qf::core::sql::QxRecChng chng { + .table = table_id, + .id = id_pri_key_value.value(), + .record = qx_record, + .op = qf::core::sql::QxRecOp::Update, + }; + emit qxRecChng(chng); + } } } } @@ -437,6 +445,17 @@ bool SqlTableModel::removeTableRow(int row_no, bool throw_exc) ret = false; break; } + if (where_rec.count() == 1 && where_rec.field(0).name() == "id") { + if (auto id = where_rec.field(0).value().toInt(); id < 0) { + qf::core::sql::QxRecChng chng { + .table = table_id, + .id = id, + .record = {}, + .op = qf::core::sql::QxRecOp::Delete, + }; + emit qxRecChng(chng); + } + } } } if(ret) { diff --git a/libqf/libqfgui/src/model/sqltablemodel.h b/libqf/libqfgui/src/model/sqltablemodel.h index 95a2cbebd..e055385f6 100644 --- a/libqf/libqfgui/src/model/sqltablemodel.h +++ b/libqf/libqfgui/src/model/sqltablemodel.h @@ -10,12 +10,10 @@ #include #include -namespace qf { -namespace gui { -namespace sql { -class Connection; -} -namespace model { +namespace qf::core::sql { struct QxRecChng; } +namespace qf::gui::sql { class Connection; } + +namespace qf::gui::model { class QFGUI_DECL_EXPORT SqlTableModel : public TableModel { @@ -51,6 +49,8 @@ class QFGUI_DECL_EXPORT SqlTableModel : public TableModel int reloadRow(int row_no) Q_DECL_OVERRIDE; int reloadInserts(const QString &id_column_name) Q_DECL_OVERRIDE; QString reloadRowQuery(const QVariant &record_id); + + Q_SIGNAL void qxRecChng(const qf::core::sql::QxRecChng &recchng); public: void setQueryBuilder(const qf::core::sql::QueryBuilder &qb, bool clear_columns = false); const qf::core::sql::QueryBuilder& queryBuilder() const; @@ -100,5 +100,5 @@ class QFGUI_DECL_EXPORT SqlTableModel : public TableModel QMap m_foreignKeyDependencies; }; -}}} +} diff --git a/libqf/libqfgui/src/tableview.cpp b/libqf/libqfgui/src/tableview.cpp index 72cae3801..bd3183e59 100644 --- a/libqf/libqfgui/src/tableview.cpp +++ b/libqf/libqfgui/src/tableview.cpp @@ -1146,7 +1146,7 @@ void TableView::rowExternallySaved(const QVariant &id) qf::core::sql::Query q; bool ok = q.exec(query_str); if (!ok) { - qfInfo() << "Query:" << query_str; + qfMessage() << "Query:" << query_str; qfWarning() << "SQL error:" << q.lastErrorText(); return; } diff --git a/libquickevent/libquickeventgui/src/reportoptionsdialog.h b/libquickevent/libquickeventgui/src/reportoptionsdialog.h index e4b22c6f9..bdccb558b 100644 --- a/libquickevent/libquickeventgui/src/reportoptionsdialog.h +++ b/libquickevent/libquickeventgui/src/reportoptionsdialog.h @@ -86,13 +86,13 @@ class QUICKEVENTGUI_DECL_EXPORT ReportOptionsDialog : public QDialog, public qf: explicit ReportOptionsDialog(QWidget *parent = nullptr); ~ReportOptionsDialog() override; - int exec() Q_DECL_OVERRIDE; + int exec() override; void setStartListPrintVacantsVisible(bool b); void setStartListForRelays(); - QString persistentSettingsPath() Q_DECL_OVERRIDE; - bool setPersistentSettingsId(const QString &id) Q_DECL_OVERRIDE; + QString persistentSettingsPath() override; + bool setPersistentSettingsId(const QString &id) override; Q_SIGNAL void persistentSettingsIdChanged(const QString &id); void setOptions(const Options &options); @@ -127,7 +127,7 @@ class QUICKEVENTGUI_DECL_EXPORT ReportOptionsDialog : public QDialog, public qf: static QString sqlWhereExpression(const Options &opts, const int stage_id); static QString getClassesForStartNumber(const int number, const int stage_id); protected: - //void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; + //void showEvent(QShowEvent *event) override; private: Ui::ReportOptionsDialog *ui; }; diff --git a/quickevent/app/quickevent/CMakeLists.txt b/quickevent/app/quickevent/CMakeLists.txt index c151402cf..29f5b352b 100644 --- a/quickevent/app/quickevent/CMakeLists.txt +++ b/quickevent/app/quickevent/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(quickevent plugins/Classes/src/editcourseswidget.cpp plugins/Classes/src/editcourseswidget.ui plugins/Classes/src/importcoursedef.cpp + plugins/Classes/src/courseitemdelegate.h plugins/Classes/src/courseitemdelegate.cpp plugins/Competitors/src/competitordocument.cpp plugins/Competitors/src/competitorwidget.cpp @@ -88,14 +89,13 @@ add_executable(quickevent plugins/Event/src/services/serviceswidget.cpp plugins/Event/src/services/servicewidget.cpp plugins/Event/src/services/servicewidget.ui - plugins/Event/src/services/qx/qxclientservice.cpp - plugins/Event/src/services/qx/qxclientservice.h - plugins/Event/src/services/qx/qxclientservicewidget.cpp - plugins/Event/src/services/qx/qxclientservicewidget.h - plugins/Event/src/services/qx/qxclientservicewidget.ui - plugins/Event/src/services/qx/qxlateregistrationswidget.h - plugins/Event/src/services/qx/qxlateregistrationswidget.cpp - plugins/Event/src/services/qx/qxlateregistrationswidget.ui + + plugins/Event/src/services/qx/qxnode.cpp + plugins/Event/src/services/qx/sqlapinode.cpp + plugins/Event/src/services/qx/nodes.cpp + plugins/Event/src/services/qx/qxeventservice.cpp plugins/Event/src/services/qx/qxeventservice.h + plugins/Event/src/services/qx/qxeventservicewidget.cpp plugins/Event/src/services/qx/qxeventservicewidget.h plugins/Event/src/services/qx/qxeventservicewidget.ui + plugins/Event/src/services/qx/qxlateregistrationswidget.h plugins/Event/src/services/qx/qxlateregistrationswidget.cpp plugins/Event/src/services/qx/qxlateregistrationswidget.ui plugins/Event/src/services/qx/runchangedialog.h plugins/Event/src/services/qx/runchangedialog.cpp plugins/Event/src/services/qx/runchangedialog.ui plugins/Event/src/services/qx/runchange.h plugins/Event/src/services/qx/runchange.cpp @@ -172,6 +172,10 @@ add_executable(quickevent plugins/Runs/Runs.qrc plugins/CardReader/CardReader.qrc + src/qx/sqlapi.cpp src/qx/sqlapi.h + src/qx/sqltablemodel.h src/qx/sqltablemodel.cpp + src/qx/sqldatadocument.h src/qx/sqldatadocument.cpp + src/appclioptions.cpp src/application.cpp src/loggerwidget.cpp @@ -269,12 +273,14 @@ qt6_add_lupdate(quickevent TS_FILES target_sources(quickevent PRIVATE ${QM_FILES}) target_include_directories(quickevent PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(quickevent PUBLIC libquickeventcore libquickeventgui libqfgui libsiut) +if (QF_WITH_LIBSHV) + target_link_libraries(quickevent PUBLIC libshviotqt) + target_compile_definitions(quickevent PRIVATE QF_WITH_LIBSHV) +endif() install(TARGETS quickevent) install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_BINDIR}/translations) -#install(DIRECTORY plugins/Runs/qml/reports DESTINATION ${CMAKE_INSTALL_BINDIR}/reports/Runs/qml) -#install(DIRECTORY plugins/Receipts/qml/reports DESTINATION ${CMAKE_INSTALL_BINDIR}/reports/Receipts/qml) foreach(plugin IN ITEMS Classes Runs Relays Receipts shared) install(DIRECTORY plugins/${plugin}/qml/reports DESTINATION ${CMAKE_INSTALL_BINDIR}/reports/${plugin}/qml) endforeach() diff --git a/quickevent/app/quickevent/plugins/CardReader/src/cardreaderwidget.cpp b/quickevent/app/quickevent/plugins/CardReader/src/cardreaderwidget.cpp index 9fd066be2..f9a6a90fa 100644 --- a/quickevent/app/quickevent/plugins/CardReader/src/cardreaderwidget.cpp +++ b/quickevent/app/quickevent/plugins/CardReader/src/cardreaderwidget.cpp @@ -112,6 +112,7 @@ class Model : public quickevent::gui::og::SqlTableModel Model::Model(QObject *parent) : Super(parent) { + setIdColumnName("cards.id"); clearColumns(col_COUNT); setColumn(col_cards_id, ColumnDefinition("cards.id", "id").setReadOnly(true)); setColumn(col_cards_siId, ColumnDefinition("cards.siId", tr("SI")).setReadOnly(true).setCastType(qMetaTypeId())); diff --git a/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp b/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp index 56faf7216..c4750dadc 100644 --- a/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp +++ b/quickevent/app/quickevent/plugins/Classes/src/classeswidget.cpp @@ -1,12 +1,15 @@ -#include "classesplugin.h" #include "classeswidget.h" #include "ui_classeswidget.h" +#include "classesplugin.h" #include "importcoursedef.h" #include "editcodeswidget.h" #include "editcourseswidget.h" #include "drawing/drawingganttwidget.h" +#include +#include + #include #include @@ -26,7 +29,6 @@ #include #include #include -#include #include #include @@ -48,70 +50,6 @@ using qf::gui::framework::getPlugin; using Event::EventPlugin; using Classes::ClassesPlugin; -class CourseItemDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - CourseItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - - Q_SIGNAL void courseIdChanged(); - - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE - { - Q_UNUSED(option) - Q_UNUSED(index) - auto *editor = new QComboBox(parent); -#if QT_VERSION_MAJOR >= 6 - QMultiMapIterator it(m_courseNameToId); -#else - QMapIterator it(m_courseNameToId); -#endif - while(it.hasNext()) { - it.next(); - editor->addItem(it.key(), it.value()); - } - return editor; - } - void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE - { - auto *cbx = qobject_cast(editor); - QF_ASSERT(cbx != nullptr, "Bad combo!", return); - QString id = index.data(Qt::EditRole).toString(); - int ix = cbx->findData(id); - cbx->setCurrentIndex(ix); - } - void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const Q_DECL_OVERRIDE - { - qfLogFuncFrame(); - auto *cbx = qobject_cast(editor); - QF_ASSERT(cbx != nullptr, "Bad combo!", return); - qfDebug() << "setting model data:" << cbx->currentText() << cbx->currentData(); - model->setData(index, cbx->currentData(), Qt::EditRole); - emit const_cast(this)->courseIdChanged(); // NOLINT(cppcoreguidelines-pro-type-const-cast) - } - - QString displayText(const QVariant &value, const QLocale &locale) const Q_DECL_OVERRIDE - { - Q_UNUSED(locale) - return m_idToCourseName.value(value.toInt(), QStringLiteral("???")); - } - - void setCourses(const QMap &courses) - { - m_idToCourseName = courses; - m_courseNameToId.clear(); - QMapIterator it(m_idToCourseName); - while(it.hasNext()) { - it.next(); - m_courseNameToId.insert(it.value(), it.key()); - } - } - -private: - QMap m_idToCourseName; - QMultiMap m_courseNameToId; -}; - class CourseCodesTableModel : public qfm::SqlTableModel { Q_OBJECT @@ -176,8 +114,8 @@ ClassesWidget::ClassesWidget(QWidget *parent) : ui->tblClassesTB->setTableView(ui->tblClasses); auto *m = new qfm::SqlTableModel(this); - //m->setObjectName("classes.classesModel"); - m->addColumn("id").setReadOnly(true); + m->setIdColumnName("classes.id"); + m->addColumn("classes.id").setReadOnly(true); m->addColumn("classes.name", tr("Class")); m->addColumn("classdefs.drawLock", tr("DL")).setToolTip(tr("Locked for drawing")); m->addColumn("classdefs.startTimeMin", tr("Start")); @@ -452,7 +390,8 @@ void ClassesWidget::importCourses(const QList &course_defs, con reload(); } -static QString normalize_course_name(const QString &course_name) +namespace { +QString normalize_course_name(const QString &course_name) { QString ret = qf::core::Collator::toAscii7(QLocale::Czech, course_name, false); ret.replace(' ', QString()); @@ -462,6 +401,7 @@ static QString normalize_course_name(const QString &course_name) ret.replace('-', '+'); return ret; } +} void ClassesWidget::import_ocad_txt() { @@ -628,7 +568,8 @@ void ClassesWidget::import_ocad_v8() } } -static QString element_text(const QDomElement &parent, const QString &tag_name) +namespace { +QString element_text(const QDomElement &parent, const QString &tag_name) { QDomElement el = parent.firstChildElement(tag_name); if(el.isNull()) @@ -636,13 +577,14 @@ static QString element_text(const QDomElement &parent, const QString &tag_name) return el.text(); } -static QString dump_element(const QDomElement &el) +QString dump_element(const QDomElement &el) { QString ret; QTextStream s(&ret); el.save(s, QDomNode::EncodingFromDocument); return ret; } +} void ClassesWidget::import_ocad_iofxml_2() { diff --git a/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.cpp b/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.cpp new file mode 100644 index 000000000..3f6c73858 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.cpp @@ -0,0 +1,81 @@ +#include "courseitemdelegate.h" + +#include + +#include +#include + +CourseItemDelegate::CourseItemDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{} + +QWidget *CourseItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + auto *editor = new QComboBox(parent); + initCombo(editor, m_idToCourseName, textImplicit()); + return editor; +} + +void CourseItemDelegate::initCombo(QComboBox *combo, const QMap &courses, const QString &null_text) +{ + combo->setInsertPolicy(QComboBox::NoInsert); + combo->setSizeAdjustPolicy(QComboBox::AdjustToContents); + combo->setMaxVisibleItems(20); + combo->setEditable(true); + QMap name_to_id; + if (!null_text.isEmpty()) { + combo->addItem(null_text, {}); + } + for (const auto &[id, name] : courses.asKeyValueRange()) { + name_to_id[name] = id; + } + for (const auto &[name, id] : name_to_id.asKeyValueRange()) { + combo->addItem(name, id); + } + // Enable filtering + auto items = name_to_id.keys(); + if (!null_text.isEmpty()) { + items.insert(0, null_text); + } + auto *completer = new QCompleter(items, combo); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + combo->setCompleter(completer); +} + +void CourseItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + auto *cbx = qobject_cast(editor); + QF_ASSERT(cbx != nullptr, "Bad combo!", return); + QString id = index.data(Qt::EditRole).toString(); + int ix = cbx->findData(id); + cbx->setCurrentIndex(ix); +} +void CourseItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + qfLogFuncFrame(); + auto *cbx = qobject_cast(editor); + QF_ASSERT(cbx != nullptr, "Bad combo!", return); + qfDebug() << "setting model data:" << cbx->currentText() << cbx->currentData(); + model->setData(index, cbx->currentData(), Qt::EditRole); + emit const_cast(this)->courseIdChanged(); // NOLINT(cppcoreguidelines-pro-type-const-cast) +} + +QString CourseItemDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + Q_UNUSED(locale) + return m_idToCourseName.value(value.toInt(), "???"); +} + +void CourseItemDelegate::setCourses(const QMap &courses) +{ + m_idToCourseName = courses; +} + +QString CourseItemDelegate::textImplicit() +{ + return tr("Implicit"); +} + diff --git a/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.h b/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.h new file mode 100644 index 000000000..d7903ca7d --- /dev/null +++ b/quickevent/app/quickevent/plugins/Classes/src/courseitemdelegate.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class QComboBox; + +class CourseItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + CourseItemDelegate(QObject *parent); + + Q_SIGNAL void courseIdChanged(); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + + QString displayText(const QVariant &value, const QLocale &locale) const override; + void setNullText(const QString &text) { m_nullText = text; } + + void setCourses(const QMap &courses); + + static QString textImplicit(); + static void initCombo(QComboBox *combo, const QMap &courses, const QString &null_text); +private: + QMap m_idToCourseName; + QString m_nullText; +}; + + diff --git a/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.h b/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.h index a394edbe4..ebca84986 100644 --- a/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.h +++ b/quickevent/app/quickevent/plugins/Competitors/src/competitordocument.h @@ -1,24 +1,22 @@ #ifndef COMPETITORS_COMPETITORDOCUMENT_H #define COMPETITORS_COMPETITORDOCUMENT_H -#include +#include "src/qx/sqldatadocument.h" #include namespace Competitors { -class CompetitorDocument : public qf::gui::model::SqlDataDocument +class CompetitorDocument : public qx::SqlDataDocument { Q_OBJECT private: - typedef qf::gui::model::SqlDataDocument Super; + typedef qx::SqlDataDocument Super; public: CompetitorDocument(QObject *parent = nullptr); - //bool isSaveSiidToRuns() const {return m_saveSiidToRuns;} void setEmitDbEventsOnSave(bool b) {m_isEmitDbEventsOnSave = b;} - //void setSiid(const QVariant &siid, bool save_siid_to_runs); void setSiid(const QVariant &siid); QVariant siid() const; const QVector& runsIds() const {return m_runsIds;} diff --git a/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml b/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml index c6a8946ac..c06846d00 100644 --- a/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml +++ b/quickevent/app/quickevent/plugins/Event/qml/DbSchema.qml @@ -191,6 +191,7 @@ Schema { //defaultValue: 0; //notNull: true }, + Field { name: 'courseId'; type: Int {} }, Field { name: 'relayId'; type: Int {} }, Field { name: 'corridorTime'; type: DateTime {} comment: 'DateTime when competitor entered start corridor. (Experimental)' diff --git a/quickevent/app/quickevent/plugins/Event/src/eventconfig.cpp b/quickevent/app/quickevent/plugins/Event/src/eventconfig.cpp index a9c15a07e..d45f0c389 100644 --- a/quickevent/app/quickevent/plugins/Event/src/eventconfig.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/eventconfig.cpp @@ -65,11 +65,12 @@ void EventConfig::load() Connection conn = Connection::forName(); // Check connection existence / validity - if(!conn.isOpen()) { - qfWarning() << "EventConfig::load(): database connection is not open:" - << conn.errorString(); - return; - } + Q_ASSERT(conn.isOpen()); + // if(!conn.isOpen()) { + // qfWarning() << "EventConfig::load(): database connection is not open:" + // << conn.errorString(); + // return; + // } Query q(conn); QueryBuilder qb; diff --git a/quickevent/app/quickevent/plugins/Event/src/eventconfig.h b/quickevent/app/quickevent/plugins/Event/src/eventconfig.h index 11819cac5..a0f876b42 100644 --- a/quickevent/app/quickevent/plugins/Event/src/eventconfig.h +++ b/quickevent/app/quickevent/plugins/Event/src/eventconfig.h @@ -35,7 +35,9 @@ class EventConfig : public QObject public: QVariantMap values() const {return m_data;} QVariant value(const QStringList &path, const QVariant &default_value = QVariant()) const; - QVariant value(const QString &path, const QVariant &default_value = QVariant()) const {return value(path.split('.'), default_value);} + QVariant value(const QString &path, const QVariant &default_value = QVariant()) const { + return value(path.split('.'), default_value); + } void setValue(const QStringList &path, const QVariant &val); void setValue(const QString &path, const QVariant &val) {setValue(path.split('.'), val);} void load(); diff --git a/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp b/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp index c8893ef72..6f7accfcc 100644 --- a/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp @@ -11,7 +11,7 @@ #include "services/serviceswidget.h" #include "services/emmaclient.h" -#include "services/qx/qxclientservice.h" +#include "services/qx/qxeventservice.h" #include #include @@ -380,7 +380,7 @@ void EventPlugin::onInstalled() auto *emma_client = new services::EmmaClient(this); services::Service::addService(emma_client); - auto shvapi_client = new services::qx::QxClientService(this); + auto shvapi_client = new services::qx::QxEventService(this); services::Service::addService(shvapi_client); { @@ -571,8 +571,10 @@ DbSchema *EventPlugin::dbSchema() int EventPlugin::dbVersion() { - // equals to minimal app version compatible with this DB - return 30301; + // equals to app version ignoring patch number + auto app_ver = QCoreApplication::applicationVersion(); + auto db_ver = (qf::core::Utils::versionStringToInt(app_ver) / 100) * 100; + return db_ver; } QString EventPlugin::dbVersionString() diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.cpp new file mode 100644 index 000000000..6a4db4533 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.cpp @@ -0,0 +1,51 @@ +#include "nodes.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace qf::core::sql; +using namespace shv::chainpack; + +namespace Event::services::qx { + +//========================================================= +// DotAppNode +//========================================================= +namespace { +auto METH_NAME = "name"; +} +const std::vector &DotAppNode::metaMethods() +{ + static std::vector meta_methods { + methods::DIR, + methods::LS, + {Rpc::METH_PING, MetaMethod::Flag::None, {}, "RpcValue", AccessLevel::Browse}, + {METH_NAME, MetaMethod::Flag::IsGetter, {}, "RpcValue", AccessLevel::Browse}, + }; + return meta_methods; +} + +RpcValue DotAppNode::callMethod(const StringViewList &shv_path, const std::string &method, const shv::chainpack::RpcValue ¶ms, const shv::chainpack::RpcValue &user_id) +{ + qfLogFuncFrame() << shv_path.join('/') << method; + //eyascore::utils::UserId user_id = eyascore::utils::UserId::makeUserName(QString::fromStdString(rq.userId().toMap().value("userName").toString())); + if(shv_path.empty()) { + if(method == Rpc::METH_PING) { + return nullptr; + } + if(method == METH_NAME) { + return "QuickEvent"; + } + } + return Super::callMethod(shv_path, method, params, user_id); +} + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.h new file mode 100644 index 000000000..58cf1125e --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/nodes.h @@ -0,0 +1,22 @@ +#pragma once + +#include "qxnode.h" + +#include + +namespace Event::services::qx { + +class DotAppNode : public QxNode +{ + Q_OBJECT + + using Super = QxNode; +public: + explicit DotAppNode(shv::iotqt::node::ShvNode *parent) : Super(".app", parent) {} +private: + //shv::chainpack::RpcValue callMethodRq(const shv::chainpack::RpcRequest &rq) override; + const std::vector &metaMethods() override; + shv::chainpack::RpcValue callMethod(const StringViewList &shv_path, const std::string &method, const shv::chainpack::RpcValue ¶ms, const shv::chainpack::RpcValue &user_id) override; +}; + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.cpp similarity index 63% rename from quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp rename to quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.cpp index 502483522..24f6bab19 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.cpp @@ -1,5 +1,8 @@ -#include "qxclientservice.h" -#include "qxclientservicewidget.h" +#include "qxeventservice.h" +#include "qxeventservicewidget.h" +#include "nodes.h" +#include "sqlapinode.h" +#include "src/qx/sqlapi.h" #include "../../eventplugin.h" #include "../../../../Runs/src/runsplugin.h" @@ -9,6 +12,10 @@ #include #include +#include +#include +#include + #include #include #include @@ -24,6 +31,10 @@ #include #include +using namespace shv::chainpack; +using namespace shv::iotqt::rpc; +using namespace shv::iotqt::node; + using namespace qf::core; using namespace qf::gui; using namespace qf::gui::dialogs; @@ -33,65 +44,65 @@ using Event::EventPlugin; using Runs::RunsPlugin; namespace Event::services::qx { -//=============================================== -// QxClientServiceSettings -//=============================================== -// QString QxClientServiceSettings::eventKey() const -// { -// auto *event_plugin = getPlugin(); -// auto *cfg = event_plugin->eventConfig(); -// auto key = cfg->apiKey(); -// auto current_stage = cfg->currentStageId(); -// return QStringLiteral("%1%2").arg(key).arg(current_stage); -// } //=============================================== // QxClientService //=============================================== -QxClientService::QxClientService(QObject *parent) - : Super(QxClientService::serviceId(), parent) +QxEventService::QxEventService(QObject *parent) + : Super(QxEventService::serviceId(), parent) + , m_rootNode(new shv::iotqt::node::ShvRootNode(this)) { - auto *event_plugin = getPlugin(); - connect(event_plugin, &Event::EventPlugin::dbEventNotify, this, &QxClientService::onDbEventNotify, Qt::QueuedConnection); + new DotAppNode(m_rootNode); + new SqlApiNode(m_rootNode); + + connect(m_rootNode, &shv::iotqt::node::ShvNode::sendRpcMessage, this, &QxEventService::sendRpcMessage); } -QString QxClientService::serviceDisplayName() const +QString QxEventService::serviceDisplayName() const { - return tr("QE Exchange"); + return tr("QX Event"); } -QString QxClientService::serviceId() +QString QxEventService::serviceId() { return QStringLiteral("qx"); } -void QxClientService::run() { +void QxEventService::run() { + using namespace shv::iotqt::rpc; + auto ss = settings(); - auto *reply = getRemoteEventInfo(ss.exchangeServerUrl(), apiToken()); - connect(reply, &QNetworkReply::finished, this, [this, reply, ss]() { - if (reply->error() == QNetworkReply::NetworkError::NoError) { - auto data = reply->readAll(); - auto doc = QJsonDocument::fromJson(data); - EventInfo event_info(doc.toVariant().toMap()); - setStatusMessage(event_info.name() + (event_info.stage_count() > 1? QStringLiteral(" E%1").arg(event_info.stage()): QString())); - m_eventId = event_info.id(); - connectToSSE(m_eventId); - if (!m_pollChangesTimer) { - m_pollChangesTimer = new QTimer(this); - connect(m_pollChangesTimer, &QTimer::timeout, this, &QxClientService::pollQxChanges); - } - pollQxChanges(); - m_pollChangesTimer->start(10000); - Super::run(); - } - else { - qfWarning() << "Cannot run QX service, network error:" << reply->errorString(); - } - }); + + delete m_rpcConnection; + m_eventId = 0; + + auto api_token = apiToken(); + if (api_token.isEmpty()) { + setStatus(Status::Stopped); + setStatusMessage(tr("API token is not set.")); + } + + m_rpcConnection = new DeviceConnection("QuickEvent", this); + m_rpcConnection->setConnectionString(ss.shvBrokerUrl()); + RpcValue::Map opts; + RpcValue::Map device; + device["deviceId"] = api_token.toStdString(); + opts["device"] = device; + m_rpcConnection->setConnectionOptions(opts); + + connect(m_rpcConnection, &ClientConnection::brokerConnectedChanged, this, &QxEventService::onBrokerConnectedChanged); + connect(m_rpcConnection, &ClientConnection::socketError, this, &QxEventService::onBrokerSocketError); + connect(m_rpcConnection, &ClientConnection::brokerLoginError, this, &QxEventService::onBrokerLoginError); + connect(m_rpcConnection, &ClientConnection::rpcMessageReceived, this, &QxEventService::onRpcMessageReceived); + + connect(::qx::SqlApi::instance(), &::qx::SqlApi::recchng, this, &QxEventService::sendRecchgShvSignal); + + m_rpcConnection->open(); } -void QxClientService::stop() +void QxEventService::stop() { + m_eventId = 0; disconnectSSE(); if (m_pollChangesTimer) { m_pollChangesTimer->stop(); @@ -99,23 +110,23 @@ void QxClientService::stop() Super::stop(); } -qf::gui::framework::DialogWidget *QxClientService::createDetailWidget() +qf::gui::framework::DialogWidget *QxEventService::createDetailWidget() { - auto *w = new QxClientServiceWidget(); + auto *w = new QxEventServiceWidget(); return w; } -void QxClientService::loadSettings() +void QxEventService::loadSettings() { Super::loadSettings(); auto ss = settings(); - if (ss.exchangeServerUrl().isEmpty()) { - ss.setExchangeServerUrl("http://localhost:8000"); + if (ss.shvBrokerUrl().isEmpty()) { + ss.setShvBrokerUrl("tcp://localhost?user=test&password=test"); } m_settings = ss; } -void QxClientService::onDbEventNotify(const QString &domain, int connection_id, const QVariant &data) +void QxEventService::onDbEventNotify(const QString &domain, int connection_id, const QVariant &data) { Q_UNUSED(connection_id) Q_UNUSED(data) @@ -162,7 +173,7 @@ void QxClientService::onDbEventNotify(const QString &domain, int connection_id, } } -QNetworkAccessManager *QxClientService::networkManager() +QNetworkAccessManager *QxEventService::networkManager() { if (!m_networkManager) { m_networkManager = new QNetworkAccessManager(this); @@ -170,7 +181,7 @@ QNetworkAccessManager *QxClientService::networkManager() return m_networkManager; } -QNetworkReply *QxClientService::getRemoteEventInfo(const QString &qxhttp_host, const QString &api_token) +QNetworkReply *QxEventService::getRemoteEventInfo(const QString &qxhttp_host, const QString &api_token) { auto *nm = networkManager(); QNetworkRequest request; @@ -181,7 +192,7 @@ QNetworkReply *QxClientService::getRemoteEventInfo(const QString &qxhttp_host, c return nm->get(request); } -QNetworkReply *QxClientService::postEventInfo(const QString &qxhttp_host, const QString &api_token) +QNetworkReply *QxEventService::postEventInfo(const QString &qxhttp_host, const QString &api_token) { auto *nm = networkManager(); QNetworkRequest request; @@ -197,7 +208,7 @@ QNetworkReply *QxClientService::postEventInfo(const QString &qxhttp_host, const return nm->post(request, data); } -void QxClientService::postStartListIofXml3(QObject *context, std::function call_back) +void QxEventService::postStartListIofXml3(QObject *context, std::function call_back) { auto *ep = getPlugin(); int current_stage = ep->currentStageId(); @@ -208,7 +219,7 @@ void QxClientService::postStartListIofXml3(QObject *context, std::function call_back) +void QxEventService::postRuns(QObject *context, std::function call_back) { auto *ep = getPlugin(); int current_stage = ep->currentStageId(); @@ -220,9 +231,9 @@ void QxClientService::postRuns(QObject *context, std::function c } } -void QxClientService::getHttpJson(const QString &path, const QUrlQuery &query, QObject *context, const std::function &call_back) +void QxEventService::getHttpJson(const QString &path, const QUrlQuery &query, QObject *context, const std::function &call_back) { - auto url = exchangeServerUrl(); + auto url = shvBrokerUrl(); url.setPath(path); url.setQuery(query); // qfInfo() << url.toString(); @@ -249,9 +260,9 @@ void QxClientService::getHttpJson(const QString &path, const QUrlQuery &query, Q }); } -QNetworkReply* QxClientService::getQxChangesReply(int from_id) +QNetworkReply* QxEventService::getQxChangesReply(int from_id) { - auto url = exchangeServerUrl(); + auto url = shvBrokerUrl(); url.setPath(QStringLiteral("/api/event/%1/changes").arg(eventId())); url.setQuery(QStringLiteral("from_id=%1").arg(from_id)); @@ -261,32 +272,32 @@ QNetworkReply* QxClientService::getQxChangesReply(int from_id) return networkManager()->get(request); } -int QxClientService::eventId() const +int QxEventService::eventId() const { - if (m_eventId == 0) { - throw qf::core::Exception(tr("Event ID is not loaded, service is not probably running.")); - } + // if (m_eventId == 0) { + // throw qf::core::Exception(tr("Event ID is not loaded, service is not probably running.")); + // } return m_eventId; } -QByteArray QxClientService::apiToken() const +QString QxEventService::apiToken() const { - // API token must not be cached to enable service point + // API token must not be cached to enable service to point // always to current stage event on qxhttpd auto *event_plugin = getPlugin(); auto current_stage = event_plugin->currentStageId(); - return event_plugin->stageData(current_stage).qxApiToken().toUtf8(); + return event_plugin->stageData(current_stage).qxApiToken(); } -QUrl QxClientService::exchangeServerUrl() const +QUrl QxEventService::shvBrokerUrl() const { auto ss = settings(); - return QUrl(ss.exchangeServerUrl()); + return QUrl(ss.shvBrokerUrl()); } -void QxClientService::postFileCompressed(std::optional path, std::optional name, QByteArray data, QObject *context , std::function call_back) +void QxEventService::postFileCompressed(std::optional path, std::optional name, QByteArray data, QObject *context , std::function call_back) { - auto url = exchangeServerUrl(); + auto url = shvBrokerUrl(); url.setPath(path.value_or("/api/event/current/file")); if (name.has_value()) { @@ -294,7 +305,7 @@ void QxClientService::postFileCompressed(std::optional path, std::optio } QNetworkRequest request; request.setUrl(url); - request.setRawHeader(QX_API_TOKEN, apiToken()); + request.setRawHeader(QX_API_TOKEN, apiToken().toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/zip")); auto zdata = zlibCompress(data); QNetworkReply *reply = networkManager()->post(request, zdata); @@ -312,7 +323,7 @@ void QxClientService::postFileCompressed(std::optional path, std::optio }); } -void QxClientService::uploadSpecFile(SpecFile file, QByteArray data, QObject *context, const std::function &call_back) +void QxEventService::uploadSpecFile(SpecFile file, QByteArray data, QObject *context, const std::function &call_back) { switch (file) { case SpecFile::StartListIofXml3: @@ -324,7 +335,7 @@ void QxClientService::uploadSpecFile(SpecFile file, QByteArray data, QObject *co } } -QByteArray QxClientService::zlibCompress(QByteArray data) +QByteArray QxEventService::zlibCompress(QByteArray data) { QByteArray compressedData = qCompress(data); // strip the 4-byte length put on by qCompress @@ -333,19 +344,19 @@ QByteArray QxClientService::zlibCompress(QByteArray data) return compressedData; } -void QxClientService::httpPostJson(const QString &path, const QString &query, QVariantMap json, QObject *context, const std::function &call_back) +void QxEventService::httpPostJson(const QString &path, const QString &query, QVariantMap json, QObject *context, const std::function &call_back) { if (!isRunning()) { return; } - auto url = exchangeServerUrl(); + auto url = shvBrokerUrl(); url.setPath(path); url.setQuery(query); QNetworkRequest request; request.setUrl(url); - request.setRawHeader(QX_API_TOKEN, apiToken()); + request.setRawHeader(QX_API_TOKEN, apiToken().toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json")); auto data = QJsonDocument::fromVariant(json).toJson(QJsonDocument::Compact); qfInfo() << "HTTP POST JSON:" << url.toString() << "data:" << QString::fromUtf8(data); @@ -373,7 +384,7 @@ void QxClientService::httpPostJson(const QString &path, const QString &query, QV } } -void QxClientService::connectToSSE(int event_id) +void QxEventService::connectToSSE(int event_id) { Q_UNUSED(event_id); // auto url = exchangeServerUrl(); @@ -396,7 +407,7 @@ void QxClientService::connectToSSE(int event_id) // }); } -void QxClientService::disconnectSSE() +void QxEventService::disconnectSSE() { if (m_replySSE) { qfInfo() << "Disconnecting SSE:" << m_replySSE; @@ -405,7 +416,7 @@ void QxClientService::disconnectSSE() } } -void QxClientService::pollQxChanges() +void QxEventService::pollQxChanges() { auto event_plugin = getPlugin(); if(!getPlugin()->isEventOpen()) { @@ -480,7 +491,7 @@ void QxClientService::pollQxChanges() } } -EventInfo QxClientService::eventInfo() const +EventInfo QxEventService::eventInfo() const { auto *event_plugin = getPlugin(); auto *event_config = event_plugin->eventConfig(); @@ -537,35 +548,125 @@ EventInfo QxClientService::eventInfo() const // qfInfo() << qf::core::Utils::qvariantToJson(ei, false); return ei; } -/* -namespace { -auto query_to_json_csv(QSqlQuery &q) +//namespace { +//auto query_to_json_csv(QSqlQuery &q) +//{ +// QVariantList csv; +// { +// // QStringList columns{"name", "control_count", "length", "climb", "start_time", "interval", "start_slot_count"}; +// QStringList columns; +// auto rec = q.record(); +// for (auto i = 0; i < rec.count(); ++i) { +// columns << rec.field(i).name(); +// } +// csv.insert(csv.length(), columns); +// } +// while (q.next()) { +// QVariantList values; +// auto rec = q.record(); +// for (auto i = 0; i < rec.count(); ++i) { +// values << q.value(i); +// } +// csv.insert(csv.length(), values); +// } +// return csv; +//} +//} +int QxEventService::currentConnectionId() { - QVariantList csv; - { - // QStringList columns{"name", "control_count", "length", "climb", "start_time", "interval", "start_slot_count"}; - QStringList columns; - auto rec = q.record(); - for (auto i = 0; i < rec.count(); ++i) { - columns << rec.field(i).name(); - } - csv.insert(csv.length(), columns); + return qf::core::sql::Connection::forName().connectionId(); +} + +void QxEventService::onBrokerConnectedChanged(bool is_connected) +{ + if(is_connected) { + auto *rpc_call = shv::iotqt::rpc::RpcCall::create(m_rpcConnection) + ->setShvPath(".broker/currentClient") + ->setMethod("info"); + connect(rpc_call, &shv::iotqt::rpc::RpcCall::maybeResult, this, [this](const ::shv::chainpack::RpcValue &result, const shv::chainpack::RpcError &error) { + if (error.isValid()) { + setStatus(Status::Stopped); + setStatusMessage(tr("Client info discovery error: %1").arg(error.toString())); + } + else { + const auto &info = result.asMap(); + m_eventMountPoint = info.value("mountPoint").to(); + m_eventId = m_eventMountPoint.section('/', -1, -1).toInt(); + setStatus(Status::Running); + setStatusMessage(tr("Event ID: %1").arg(m_eventId)); + subscribeChanges(); + } + }); + rpc_call->start(); + } else { + setStatus(Status::Stopped); } - while (q.next()) { - QVariantList values; - auto rec = q.record(); - for (auto i = 0; i < rec.count(); ++i) { - values << q.value(i); + +} + +void QxEventService::onBrokerSocketError(const QString &err) +{ + qfWarning() << "onBrokerSocketError:" << err; + setStatusMessage(tr("Broker socket error: %1").arg(err)); +} + +void QxEventService::onBrokerLoginError(const shv::chainpack::RpcError &err) +{ + qfWarning() << "onBrokerLoginError:" << err.toString(); + setStatusMessage(tr("Broker login error: %1").arg(err.toString())); +} + +void QxEventService::onRpcMessageReceived(const shv::chainpack::RpcMessage &msg) +{ +// shvLogFuncFrame() << msg.toCpon(); + if(msg.isRequest()) { + RpcRequest rq(msg); + if (rq.shvPath().asString().starts_with(".broker/")) { + // ignore broker discovery messages + return; } - csv.insert(csv.length(), values); + qfMessage() << "RPC request received:" << rq.toPrettyString(); + m_rootNode->handleRpcRequest(rq); + } + else if(msg.isResponse()) { + RpcResponse rp(msg); + qfMessage() << "RPC response received:" << rp.toPrettyString(); + } + else if(msg.isSignal()) { + RpcSignal nt(msg); + qfMessage() << "RPC signal received:" << nt.toPrettyString(); } - return csv; } + +void QxEventService::sendRpcMessage(const shv::chainpack::RpcMessage &rpc_msg) +{ + if(m_rpcConnection && m_rpcConnection->isBrokerConnected()) { + m_rpcConnection->sendRpcMessage(rpc_msg); + } } -*/ -int QxClientService::currentConnectionId() + +void QxEventService::subscribeChanges() { - return qf::core::sql::Connection::forName().connectionId(); + Q_ASSERT(m_rpcConnection); + QString shv_path = "test"; + QString signal_name = shv::chainpack::Rpc::SIG_VAL_CHANGED; + auto *rpc_call = RpcCall::createSubscriptionRequest(m_rpcConnection, shv_path, signal_name); + connect(rpc_call, &RpcCall::maybeResult, this, [shv_path, signal_name](const ::shv::chainpack::RpcValue &result, const shv::chainpack::RpcError &error) { + if(error.isValid()) { + qfError() << "Signal:" << signal_name << "on SHV path:" << shv_path << "subscribe error:" << error.toString(); + } + else { + qfMessage() << "Signal:" << signal_name << "on SHV path:" << shv_path << "subscribed successfully" << result.toCpon(); + } + }); + rpc_call->start(); +} + +void QxEventService::sendRecchgShvSignal(const qf::core::sql::QxRecChng &chng) +{ + if (isRunning()) { + m_rpcConnection->sendShvSignal("sql", "recchng", ::qx::qxRecChngToRpcValue(chng)); + } } } // namespace Event::services::qx diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.h similarity index 68% rename from quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h rename to quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.h index 636a54c4d..add24852e 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservice.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservice.h @@ -7,15 +7,20 @@ class QNetworkReply; class QUrlQuery; class QTimer; +namespace shv::iotqt::node { class ShvNodeTree; class ShvRootNode; } +namespace shv::iotqt::rpc { class DeviceConnection; } +namespace shv::chainpack { class RpcMessage; class RpcError; } +namespace qf::core::sql { struct QxRecChng; } + namespace Event::services::qx { -class QxClientServiceSettings : public ServiceSettings +class QxEventServiceSettings : public ServiceSettings { using Super = ServiceSettings; - QF_VARIANTMAP_FIELD2(QString, e, setE, xchangeServerUrl, "http://localhost:8000") + QF_VARIANTMAP_FIELD2(QString, s, setS, hvBrokerUrl, "tcp://localhost?user=test&password=test") public: - QxClientServiceSettings(const QVariantMap &o = QVariantMap()) : Super(o) {} + QxEventServiceSettings(const QVariantMap &o = QVariantMap()) : Super(o) {} }; class EventInfo : public QVariantMap @@ -34,7 +39,7 @@ class EventInfo : public QVariantMap EventInfo(const QVariantMap &data = QVariantMap()) : QVariantMap(data) {} }; -class QxClientService : public Service +class QxEventService : public Service { Q_OBJECT @@ -42,14 +47,14 @@ class QxClientService : public Service public: static constexpr auto QX_API_TOKEN = "qx-api-token"; public: - QxClientService(QObject *parent); + QxEventService(QObject *parent); static QString serviceId(); QString serviceDisplayName() const override; void run() override; void stop() override; - QxClientServiceSettings settings() const {return QxClientServiceSettings(m_settings);} + QxEventServiceSettings settings() const {return QxEventServiceSettings(m_settings);} void onDbEventNotify(const QString &domain, int connection_id, const QVariant &data); QNetworkAccessManager* networkManager(); @@ -63,11 +68,20 @@ class QxClientService : public Service QNetworkReply* getQxChangesReply(int from_id); - QByteArray apiToken() const; + QString apiToken() const; static int currentConnectionId(); - QUrl exchangeServerUrl() const; + QUrl shvBrokerUrl() const; int eventId() const; +private: // shv + void onBrokerConnectedChanged(bool is_connected); + void onRpcMessageReceived(const shv::chainpack::RpcMessage &msg); + void sendRpcMessage(const shv::chainpack::RpcMessage &rpc_msg); + void onBrokerSocketError(const QString &err); + void onBrokerLoginError(const shv::chainpack::RpcError &err); + void sendRecchgShvSignal(const qf::core::sql::QxRecChng &chng); + + void subscribeChanges(); private: void loadSettings() override; qf::gui::framework::DialogWidget *createDetailWidget() override; @@ -84,10 +98,14 @@ class QxClientService : public Service void pollQxChanges(); EventInfo eventInfo() const; +private: // shv + shv::iotqt::rpc::DeviceConnection *m_rpcConnection = nullptr; + shv::iotqt::node::ShvRootNode *m_rootNode; private: QNetworkAccessManager *m_networkManager = nullptr; QNetworkReply *m_replySSE = nullptr; int m_eventId = 0; + QString m_eventMountPoint; QTimer *m_pollChangesTimer = nullptr; }; diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.cpp similarity index 51% rename from quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp rename to quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.cpp index e9a04902e..d18cd0e38 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.cpp @@ -1,6 +1,7 @@ -#include "qxclientservicewidget.h" -#include "ui_qxclientservicewidget.h" -#include "qxclientservice.h" +#include "qxeventservicewidget.h" +#include "ui_qxeventservicewidget.h" + +#include "qxeventservice.h" #include @@ -8,6 +9,10 @@ #include #include +#include +#include +#include + #include #include #include @@ -19,14 +24,14 @@ using qf::gui::framework::getPlugin; namespace Event::services::qx { -QxClientServiceWidget::QxClientServiceWidget(QWidget *parent) +QxEventServiceWidget::QxEventServiceWidget(QWidget *parent) : Super(parent) - , ui(new Ui::QxClientServiceWidget) + , ui(new Ui::QxEventServiceWidget) { - setPersistentSettingsId("QxClientServiceWidget"); + setPersistentSettingsId("QxEventServiceWidget"); ui->setupUi(this); - connect(ui->edServerUrl, &QLineEdit::textChanged, this, &QxClientServiceWidget::updateOCheckListPostUrl); - connect(ui->edApiToken, &QLineEdit::textChanged, this, &QxClientServiceWidget::updateOCheckListPostUrl); + connect(ui->edServerUrl, &QLineEdit::textChanged, this, &QxEventServiceWidget::updateOCheckListPostUrl); + connect(ui->edApiToken, &QLineEdit::textChanged, this, &QxEventServiceWidget::updateOCheckListPostUrl); setMessage(""); @@ -35,21 +40,22 @@ QxClientServiceWidget::QxClientServiceWidget(QWidget *parent) auto *event_plugin = getPlugin(); auto current_stage = event_plugin->currentStageId(); auto settings = svc->settings(); - ui->edServerUrl->setText(settings.exchangeServerUrl()); + ui->edServerUrl->setText(settings.shvBrokerUrl()); ui->edApiToken->setText(svc->apiToken()); ui->edCurrentStage->setValue(current_stage); - connect(ui->btTestConnection, &QAbstractButton::clicked, this, &QxClientServiceWidget::testConnection); - connect(ui->btExportEventInfo, &QAbstractButton::clicked, this, &QxClientServiceWidget::exportEventInfo); - connect(ui->btExportStartList, &QAbstractButton::clicked, this, &QxClientServiceWidget::exportStartList); - connect(ui->btExportRuns, &QAbstractButton::clicked, this, &QxClientServiceWidget::exportRuns); + ui->edEventId->setValue(svc->eventId()); + connect(ui->btTestConnection, &QAbstractButton::clicked, this, &QxEventServiceWidget::testConnection); + connect(ui->btExportEventInfo, &QAbstractButton::clicked, this, &QxEventServiceWidget::exportEventInfo); + connect(ui->btExportStartList, &QAbstractButton::clicked, this, &QxEventServiceWidget::exportStartList); + connect(ui->btExportRuns, &QAbstractButton::clicked, this, &QxEventServiceWidget::exportRuns); } -QxClientServiceWidget::~QxClientServiceWidget() +QxEventServiceWidget::~QxEventServiceWidget() { delete ui; } -void QxClientServiceWidget::setMessage(const QString &msg, MessageType msg_type) +void QxEventServiceWidget::setMessage(const QString &msg, MessageType msg_type) { if (msg.isEmpty()) { ui->lblStatus->setStyleSheet({}); @@ -70,7 +76,7 @@ void QxClientServiceWidget::setMessage(const QString &msg, MessageType msg_type) ui->lblStatus->setText(msg); } -bool QxClientServiceWidget::acceptDialogDone(int result) +bool QxEventServiceWidget::acceptDialogDone(int result) { if(result == QDialog::Accepted) { if(!saveSettings()) { @@ -80,19 +86,19 @@ bool QxClientServiceWidget::acceptDialogDone(int result) return true; } -QxClientService *QxClientServiceWidget::service() +QxEventService *QxEventServiceWidget::service() { - auto *svc = qobject_cast(Service::serviceByName(QxClientService::serviceId())); - QF_ASSERT(svc, QxClientService::serviceId() + " doesn't exist", return nullptr); + auto *svc = qobject_cast(Service::serviceByName(QxEventService::serviceId())); + QF_ASSERT(svc, QxEventService::serviceId() + " doesn't exist", return nullptr); return svc; } -bool QxClientServiceWidget::saveSettings() +bool QxEventServiceWidget::saveSettings() { auto *svc = service(); if(svc) { auto ss = svc->settings(); - ss.setExchangeServerUrl(ui->edServerUrl->text()); + ss.setShvBrokerUrl(ui->edServerUrl->text()); svc->setSettings(ss); auto *event_plugin = getPlugin(); @@ -104,34 +110,59 @@ bool QxClientServiceWidget::saveSettings() return true; } -void QxClientServiceWidget::updateOCheckListPostUrl() +void QxEventServiceWidget::updateOCheckListPostUrl() { auto url = QStringLiteral("%1/api/event/current/oc").arg(ui->edServerUrl->text()); ui->edOChecklistUrl->setText(url); ui->edOChecklistUrlHeader->setText(QStringLiteral("qx-api-token=%1").arg(ui->edApiToken->text())); } -void QxClientServiceWidget::testConnection() +void QxEventServiceWidget::testConnection() { - auto *svc = service(); - Q_ASSERT(svc); - auto *reply = svc->getRemoteEventInfo(ui->edServerUrl->text(), ui->edApiToken->text()); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - if (reply->error() == QNetworkReply::NetworkError::NoError) { - auto data = reply->readAll(); - auto doc = QJsonDocument::fromJson(data); - EventInfo event_info(doc.toVariant().toMap()); - ui->edEventId->setValue(event_info.id()); - setMessage(tr("Connected OK")); - } - else { - setMessage(tr("Connection error: %1").arg(reply->errorString()), MessageType::Error); + using namespace shv::iotqt::rpc; + using namespace shv::chainpack; + + delete findChild(); + + auto *rpc = new DeviceConnection("QuickEventTest", this); + rpc->setConnectionString(ui->edServerUrl->text()); + RpcValue::Map opts; + RpcValue::Map device; + device["deviceId"] = ui->edApiToken->text().toStdString(); + opts["device"] = device; + rpc->setConnectionOptions(opts); + + connect(rpc, &ClientConnection::brokerConnectedChanged, this, [this, rpc](bool is_connected) { + if (is_connected) { + setMessage(tr("Broker connected OK")); + auto *rpc_call = shv::iotqt::rpc::RpcCall::create(rpc) + ->setShvPath(".broker/currentClient") + ->setMethod("info"); + connect(rpc_call, &shv::iotqt::rpc::RpcCall::maybeResult, this, [this](const ::shv::chainpack::RpcValue &result, const shv::chainpack::RpcError &error) { + if (error.isValid()) { + setMessage(tr("Client info discovery error: %1").arg(error.toString()), MessageType::Error); + } + else { + const auto &info = result.asMap(); + auto mount_point = info.value("mountPoint").to(); + auto event_id = mount_point.section('/', -1, -1).toInt(); + ui->edEventId->setValue(event_id); + setMessage(tr("Event mounted at: %1, event id: %2").arg(mount_point).arg(event_id)); + } + }); + rpc_call->start(); } - reply->deleteLater(); }); + connect(rpc, &ClientConnection::socketError, this, [this](const QString &error) { + setMessage(tr("Connection error: %1").arg(error), MessageType::Error); + }); + connect(rpc, &ClientConnection::brokerLoginError, this, [this](const auto &error) { + setMessage(tr("Login error: %1").arg(QString::fromStdString(error.toString())), MessageType::Error); + }); + rpc->open(); } -void QxClientServiceWidget::exportEventInfo() +void QxEventServiceWidget::exportEventInfo() { auto *svc = service(); Q_ASSERT(svc); @@ -151,7 +182,7 @@ void QxClientServiceWidget::exportEventInfo() }); } -void QxClientServiceWidget::exportStartList() +void QxEventServiceWidget::exportStartList() { auto *svc = service(); Q_ASSERT(svc); @@ -167,7 +198,7 @@ void QxClientServiceWidget::exportStartList() }); } -void QxClientServiceWidget::exportRuns() +void QxEventServiceWidget::exportRuns() { auto *svc = service(); Q_ASSERT(svc); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.h similarity index 65% rename from quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h rename to quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.h index f1b108a30..19454ad86 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.h @@ -5,23 +5,23 @@ namespace Event::services::qx { namespace Ui { -class QxClientServiceWidget; +class QxEventServiceWidget; } -class QxClientService; +class QxEventService; -class QxClientServiceWidget : public qf::gui::framework::DialogWidget +class QxEventServiceWidget : public qf::gui::framework::DialogWidget { Q_OBJECT using Super = qf::gui::framework::DialogWidget; public: - explicit QxClientServiceWidget(QWidget *parent = nullptr); - ~QxClientServiceWidget() override; + explicit QxEventServiceWidget(QWidget *parent = nullptr); + ~QxEventServiceWidget() override; private: enum class MessageType { Ok, Error, Progress }; void setMessage(const QString &msg = {}, MessageType msg_type = MessageType::Ok); - QxClientService* service(); + QxEventService* service(); bool saveSettings(); void updateOCheckListPostUrl(); void testConnection(); @@ -29,7 +29,7 @@ class QxClientServiceWidget : public qf::gui::framework::DialogWidget void exportStartList(); void exportRuns(); private: - Ui::QxClientServiceWidget *ui; + Ui::QxEventServiceWidget *ui; bool acceptDialogDone(int result) override; }; diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.ui b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.ui similarity index 97% rename from quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.ui rename to quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.ui index d8824707e..5f4a41e1a 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxclientservicewidget.ui +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxeventservicewidget.ui @@ -1,7 +1,7 @@ - Event::services::qx::QxClientServiceWidget - + Event::services::qx::QxEventServiceWidget + 0 @@ -19,7 +19,7 @@ - Exchange server url + SHV broker url diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp index f0747e77a..083af5d69 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.cpp @@ -1,7 +1,7 @@ #include "qxlateregistrationswidget.h" #include "ui_qxlateregistrationswidget.h" -#include "qxclientservice.h" +#include "qxeventservice.h" #include "runchangedialog.h" #include "runchange.h" @@ -152,9 +152,9 @@ void QxLateRegistrationsWidget::onVisibleChanged(bool is_visible) } } -QxClientService *QxLateRegistrationsWidget::service() +QxEventService *QxLateRegistrationsWidget::service() { - auto *svc = qobject_cast(Event::services::Service::serviceByName(QxClientService::serviceId())); + auto *svc = qobject_cast(Event::services::Service::serviceByName(QxEventService::serviceId())); Q_ASSERT(svc); return svc; } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h index 6c972c739..ea906b649 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxlateregistrationswidget.h @@ -10,7 +10,7 @@ namespace Ui { class QxLateRegistrationsWidget; } -class QxClientService; +class QxEventService; class QxLateRegistrationsWidget : public QWidget { @@ -23,7 +23,7 @@ class QxLateRegistrationsWidget : public QWidget void onDbEventNotify(const QString &domain, int connection_id, const QVariant &payload); void onVisibleChanged(bool is_visible); private: - QxClientService* service(); + QxEventService* service(); void reload(); void addQxChangeRow(int sql_id); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.cpp new file mode 100644 index 000000000..dfcea61fd --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.cpp @@ -0,0 +1,36 @@ +#include "qxnode.h" + +#include +#include + +#include + +using namespace shv::chainpack; + +namespace Event::services::qx { + +QxNode::QxNode(const std::string &name, shv::iotqt::node::ShvNode *parent) + : Super(name, parent) +{ + +} + +size_t QxNode::methodCount(const StringViewList &shv_path) +{ + if(shv_path.empty()) { + return metaMethods().size(); + } + return Super::methodCount(shv_path); +} + +const MetaMethod *QxNode::metaMethod(const StringViewList &shv_path, size_t ix) +{ + if(shv_path.empty()) { + if(metaMethods().size() <= ix) + QF_EXCEPTION("Invalid method index: " + QString::number(ix) + " of: " + QString::number(metaMethods().size())); + return &(metaMethods()[ix]); + } + return Super::metaMethod(shv_path, ix); +} + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.h new file mode 100644 index 000000000..79e42c174 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/qxnode.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace Event::services::qx { + +class QxNode : public shv::iotqt::node::ShvNode +{ + Q_OBJECT + + using Super = shv::iotqt::node::ShvNode; +public: + explicit QxNode(const std::string &name, shv::iotqt::node::ShvNode *parent); + + size_t methodCount(const StringViewList &shv_path) override; + const shv::chainpack::MetaMethod* metaMethod(const StringViewList &shv_path, size_t ix) override; +protected: + //shv::chainpack::RpcValue callMethodRq(const shv::chainpack::RpcRequest &rq) override; + //shv::chainpack::RpcValue callMethod(const StringViewList &shv_path, const std::string &method, const shv::chainpack::RpcValue ¶ms, const shv::chainpack::RpcValue &user_id) override; + virtual const std::vector &metaMethods() = 0; +}; + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp index c44c38a94..e2c50edd5 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.cpp @@ -1,7 +1,7 @@ #include "runchangedialog.h" #include "ui_runchangedialog.h" -#include "qxclientservice.h" +#include "qxeventservice.h" #include "runchange.h" #include @@ -72,9 +72,9 @@ RunChangeDialog::~RunChangeDialog() delete ui; } -QxClientService *RunChangeDialog::service() +QxEventService *RunChangeDialog::service() { - auto *svc = qobject_cast(Service::serviceByName(QxClientService::serviceId())); + auto *svc = qobject_cast(Service::serviceByName(QxEventService::serviceId())); Q_ASSERT(svc); return svc; } @@ -208,7 +208,7 @@ void RunChangeDialog::resolveChanges(bool is_accepted) auto *svc = service(); auto *nm = svc->networkManager(); QNetworkRequest request; - auto url = svc->exchangeServerUrl(); + auto url = svc->shvBrokerUrl(); // qfInfo() << "url " << url.toString(); url.setPath("/api/event/current/changes/resolve-change"); @@ -220,7 +220,7 @@ void RunChangeDialog::resolveChanges(bool is_accepted) url.setQuery(query); request.setUrl(url); - request.setRawHeader(QxClientService::QX_API_TOKEN, svc->apiToken()); + request.setRawHeader(QxEventService::QX_API_TOKEN, svc->apiToken().toUtf8()); auto *reply = nm->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() == QNetworkReply::NetworkError::NoError) { diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h index d704a1d48..15a9bf147 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/runchangedialog.h @@ -14,7 +14,7 @@ class RunChangeDialog; } struct RunChange; -class QxClientService; +class QxEventService; class RunChangeDialog : public QDialog { @@ -25,7 +25,7 @@ class RunChangeDialog : public QDialog ~RunChangeDialog() override; private: - QxClientService* service(); + QxEventService* service(); void setMessage(const QString &msg, bool error); void loadOrigValues(); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.cpp b/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.cpp new file mode 100644 index 000000000..bbeeb3b08 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.cpp @@ -0,0 +1,126 @@ +#include "sqlapinode.h" +#include "src/qx/sqlapi.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace shv::chainpack; +using namespace shv::coreqt::data; + +namespace Event::services::qx { + +SqlApiNode::SqlApiNode(shv::iotqt::node::ShvNode *parent) + : Super("sql", parent) +{ +} + +namespace { +auto METH_QUERY = "query"; +auto METH_EXEC = "exec"; +auto METH_TRANSACTION = "transaction"; +auto METH_LIST = "list"; +auto METH_CREATE = "create"; +auto METH_READ = "read"; +auto METH_UPDATE = "update"; +auto METH_DELETE = "delete"; +} + +const std::vector &SqlApiNode::metaMethods() +{ + static std::vector meta_methods { + methods::DIR, + methods::LS, + {METH_QUERY, MetaMethod::Flag::LargeResultHint, "[s:query,{s|i|b|t|n}:params]", "{{s:name}:fields,[[s|i|b|t|n]]:rows}", AccessLevel::Read}, + {METH_EXEC, MetaMethod::Flag::None, "[s:query,{s|i|b|t|n}:params]", "{i:rows_affected,i|n:insert_id}", AccessLevel::Write}, + {METH_TRANSACTION, MetaMethod::Flag::None, "[s:query,[{s|i|b|t|n}]:params]", "n", AccessLevel::Write}, + {METH_LIST, MetaMethod::Flag::LargeResultHint, "{s:table,[s]|n:fields,i|n:ids_above,i|n:limit}", "[{s|i|b|t|n}]", AccessLevel::Write}, + {METH_CREATE, MetaMethod::Flag::None, "{s:table,{s|i|b|t|n}:record}", "i", AccessLevel::Write}, + {METH_READ, MetaMethod::Flag::None, "{s:table,i:id,{s}|n:fields}", "{s|i|b|t|n}|n", AccessLevel::Read}, + {METH_UPDATE, MetaMethod::Flag::None, "{s:table,i:id,{s|i|b|t|n}:record}", "b", AccessLevel::Write}, + {METH_DELETE, MetaMethod::Flag::None, "{s:table,i:id}", "b", AccessLevel::Write}, + }; + return meta_methods; +} + +RpcValue SqlApiNode::callMethod(const StringViewList &shv_path, const std::string &method, const shv::chainpack::RpcValue ¶ms, const shv::chainpack::RpcValue &user_id) +{ + qfLogFuncFrame() << shv_path.join('/') << method; + //eyascore::utils::UserId user_id = eyascore::utils::UserId::makeUserName(QString::fromStdString(rq.userId().toMap().value("userName").toString())); + if(shv_path.empty()) { + if(method == METH_EXEC) { + auto res = ::qx::SqlApi::exec(::qx::SqlQueryAndParams::fromRpcValue(params)); + return res.toRpcValue(); + } + if(method == METH_QUERY) { + auto res = ::qx::SqlApi::exec(::qx::SqlQueryAndParams::fromRpcValue(params)); + return res.toRpcValue(); + } + if(method == METH_TRANSACTION) { + auto sql_query = params.asList().valref(0).asString(); + const auto &sql_params = params.asList().valref(0); + ::qx::SqlApi::transaction(sql_query, sql_params.asList()); + return RpcValue(nullptr); + } + if(method == METH_LIST) { + const auto &map = params.asMap(); + const auto &table = map.value("table").asString(); + std::vector fields; + for (const auto &fn : map.valref("fields").asList()) { + fields.push_back(fn.asString()); + } + auto ids_above = map.contains("ids_above")? std::optional(map.value("ids_above").toInt64()): std::optional(); + auto limit = map.contains("limit")? std::optional(map.value("limit").toInt64()): std::optional(); + auto res = ::qx::SqlApi::list(table, fields, ids_above, limit); + return res.toRecordList(); + } + if(method == METH_CREATE) { + const auto &map = params.asMap(); + const auto &table = map.value("table").asString(); + const auto &record = map.valref("record").asMap(); + auto res = ::qx::SqlApi::create(table, record); + return res; + } + if(method == METH_READ) { + const auto &map = params.asMap(); + const auto &table = map.value("table").asString(); + auto id = map.value("id").toInt64(); + std::vector fields; + for (const auto &fn : map.valref("fields").asList()) { + fields.push_back(fn.asString()); + } + auto res = ::qx::SqlApi::read(table, id, fields); + if (res.has_value()) { + return res.value(); + } + return RpcValue(nullptr); + } + if(method == METH_UPDATE) { + const auto &map = params.asMap(); + const auto &table = map.value("table").asString(); + auto id = map.value("id").toInt64(); + const auto &record = map.valref("record").asMap(); + auto res = ::qx::SqlApi::update(table, id, record); + return res; + } + if(method == METH_DELETE) { + const auto &map = params.asMap(); + const auto &table = map.value("table").asString(); + auto id = map.valref("id").toInt(); + auto res = ::qx::SqlApi::drop(table, id); + return res; + } + } + return Super::callMethod(shv_path, method, params, user_id); +} + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.h b/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.h new file mode 100644 index 000000000..a36f7ed8c --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/qx/sqlapinode.h @@ -0,0 +1,24 @@ +#pragma once + +#include "qxnode.h" + +class QSqlQuery; +class QSqlRecord; + +namespace shv::coreqt::data { class RpcSqlResult; } + +namespace Event::services::qx { + +class SqlApiNode : public QxNode +{ + Q_OBJECT + + using Super = QxNode; +public: + explicit SqlApiNode(shv::iotqt::node::ShvNode *parent); +protected: + const std::vector &metaMethods() override; + shv::chainpack::RpcValue callMethod(const StringViewList &shv_path, const std::string &method, const shv::chainpack::RpcValue ¶ms, const shv::chainpack::RpcValue &user_id) override; +}; + +} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/serviceswidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/serviceswidget.cpp index e4c9a604a..324dd0424 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/serviceswidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/serviceswidget.cpp @@ -35,12 +35,7 @@ void ServicesWidget::reload() Service *svc = Service::serviceAt(i); auto *sw = new ServiceWidget(); - sw->setStatus(svc->status()); - connect(svc, &Service::statusChanged, sw, &ServiceWidget::setStatus); - sw->setServiceId(svc->serviceId(), svc->serviceDisplayName()); - sw->setMessage(svc->statusMessage()); - connect(svc, &Service::statusMessageChanged, sw, &ServiceWidget::setMessage); - connect(sw, &ServiceWidget::setRunningRequest, svc, &Service::setRunning); + sw->setService(svc); ly2->addWidget(sw); } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.cpp index dd4bf8233..3069eb848 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.cpp @@ -45,10 +45,15 @@ void ServiceWidget::setStatus(Service::Status st) } } -void ServiceWidget::setServiceId(const QString &id, const QString &display_name) +void ServiceWidget::setService(Service *service) { - m_serviceId = id; - ui->lblServiceName->setText(display_name.isEmpty()? id: display_name); + m_serviceId = service->serviceId(); + ui->lblServiceName->setText(service->serviceDisplayName()); + setStatus(service->status()); + connect(service, &Service::statusChanged, this, &ServiceWidget::setStatus); + setMessage(service->statusMessage()); + connect(service, &Service::statusMessageChanged, this, &ServiceWidget::setMessage); + connect(this, &ServiceWidget::setRunningRequest, service, &Service::setRunning); } QString ServiceWidget::serviceId() const diff --git a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.h b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.h index f659cafde..4d589d2fe 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.h @@ -21,7 +21,7 @@ class ServiceWidget : public QWidget ~ServiceWidget(); void setStatus(Service::Status st); - void setServiceId(const QString &id, const QString &display_name); + void setService(Service *service); QString serviceId() const; void setMessage(const QString &m); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.ui b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.ui index cdff60fa1..510d437a7 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.ui +++ b/quickevent/app/quickevent/plugins/Event/src/services/servicewidget.ui @@ -87,6 +87,9 @@ neco neco + + true + diff --git a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp index 7f77820c4..6435e251b 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp @@ -283,7 +283,8 @@ quickevent::core::CourseDef RunsPlugin::courseForCourseId(int course_id) int RunsPlugin::courseForRun_Classic(int run_id) { qfs::QueryBuilder qb; - qb.select("classdefs.courseId") + qb.select("classdefs.courseId AS implicitCourseId") + .select("runs.courseId AS explicitCourseId") .from("runs") .join("runs.competitorId", "competitors.id") .joinRestricted("competitors.classId", "classdefs.classId", "classdefs.stageId=runs.stageId") @@ -297,7 +298,9 @@ int RunsPlugin::courseForRun_Classic(int run_id) qfError() << "more courses found for run_id:" << run_id; return 0; } - ret = q.value(0).toInt(); + auto implicit_course_id = q.value("implicitCourseId").toInt(); + auto explicit_course_id = q.value("explicitCourseId").toInt(); + ret = explicit_course_id > 0 ? explicit_course_id : implicit_course_id; cnt++; } return ret; @@ -1295,7 +1298,7 @@ qf::core::sql::QueryBuilder RunsPlugin::runsQuery(int stage_id, int class_id, bo qfs::QueryBuilder qb; qb.select2("runs", "*") .select2("classes", "name") - .select2("competitors", "id, iofId, registration, licence, ranking, siId, note") + .select2("competitors", "id, firstName, lastName, iofId, registration, licence, ranking, siId, note") .select("COALESCE(lastName, '') || ' ' || COALESCE(firstName, '') AS competitorName") .select("lentcards.siid IS NOT NULL AS cardInLentTable") diff --git a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp index 45775f89f..5f2dfccda 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.cpp @@ -1,8 +1,10 @@ #include "runstablemodel.h" +#include "../../Event/src/eventplugin.h" +#include "src/qx/sqlapi.h" + #include #include -#include "../../Event/src/eventplugin.h" #include #include @@ -29,6 +31,7 @@ RunsTableModel::RunsTableModel(QObject *parent) setColumn(col_runs_leg, ColumnDefinition("runs.leg", tr("Leg"))); setColumn(col_classes_name, ColumnDefinition("classes.name", tr("Class"))); setColumn(col_startNumber, ColumnDefinition("startNumber", tr("SN", "start number")).setToolTip(tr("Start number"))); + setColumn(col_course_id, ColumnDefinition("runs.courseId", tr("Course"))); setColumn(col_competitors_siId, ColumnDefinition("competitors.siId", tr("SI")).setToolTip(tr("Registered SI")).setReadOnly(true)); setColumn(col_competitorName, ColumnDefinition("competitorName", tr("Name"))); setColumn(col_registration, ColumnDefinition("registration", tr("Reg"))); @@ -49,13 +52,6 @@ RunsTableModel::RunsTableModel(QObject *parent) connect(this, &RunsTableModel::dataChanged, this, &RunsTableModel::onDataChanged, Qt::QueuedConnection); } -QVariant RunsTableModel::data(const QModelIndex &index, int role) const -{ - QVariant ret; - ret = Super::data(index, role); - return ret; -} - Qt::ItemFlags RunsTableModel::flags(const QModelIndex &index) const { Qt::ItemFlags flgs = Super::flags(index); @@ -64,11 +60,35 @@ Qt::ItemFlags RunsTableModel::flags(const QModelIndex &index) const flgs = Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | flgs; //qfInfo() << flgs; } + if(index.column() == col_course_id) { + if (getPlugin()->eventConfig()->isRelays()) { + flgs &= ~Qt::ItemIsEditable; + } + } return flgs; } +QVariant RunsTableModel::data(const QModelIndex &index, int role) const +{ + if(index.column() == col_course_id && role == Qt::DisplayRole) { + if (getPlugin()->eventConfig()->isRelays()) { + auto start_number = value(index.row(), "startNumber").toInt(); + auto leg = value(index.row(), "runs.leg").toInt(); + return QStringLiteral("%1.%2").arg(start_number).arg(leg); + } + } + + return Super::data(index, role); +} + QVariant RunsTableModel::value(int row_ix, int column_ix) const { + if(column_ix == col_competitorName) { + qf::core::utils::TableRow row = tableRow(row_ix); + auto first_name = row.value(QStringLiteral("firstName")).toString(); + auto last_name = row.value(QStringLiteral("lastName")).toString(); + return last_name + ' ' + first_name; + } if(column_ix == col_runFlags) { qf::core::utils::TableRow row = tableRow(row_ix); bool mis_punch = row.value(QStringLiteral("runs.misPunch")).toBool(); diff --git a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.h b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.h index e58e1618b..94b1ca389 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.h +++ b/quickevent/app/quickevent/plugins/Runs/src/runstablemodel.h @@ -1,13 +1,13 @@ #ifndef RUNSTABLEMODEL_H #define RUNSTABLEMODEL_H -#include +#include "src/qx/sqltablemodel.h" -class RunsTableModel : public quickevent::gui::og::SqlTableModel +class RunsTableModel : public ::qx::SqlTableModel { Q_OBJECT private: - using Super = quickevent::gui::og::SqlTableModel; + using Super = ::qx::SqlTableModel; public: enum Columns { col_runs_isRunning = 0, @@ -15,6 +15,7 @@ class RunsTableModel : public quickevent::gui::og::SqlTableModel col_relays_name, col_runs_leg, col_classes_name, + col_course_id, col_startNumber, col_competitors_siId, col_competitorName, @@ -38,9 +39,9 @@ class RunsTableModel : public quickevent::gui::og::SqlTableModel RunsTableModel(QObject *parent = nullptr); int columnCount(const QModelIndex &) const override { return col_COUNT; } + Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; //bool setData(const QModelIndex &index, const QVariant &value, int role) override; - Qt::ItemFlags flags(const QModelIndex &index) const override; using Super::value; QVariant value(int row_ix, int column_ix) const override; diff --git a/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.cpp b/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.cpp index 302c8a7b5..969a9c671 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.cpp @@ -7,6 +7,14 @@ #include "runflagsdialog.h" #include "cardflagsdialog.h" +#include "src/qx/sqlapi.h" + +#include +#include +#include +#include +#include + #include #include @@ -15,13 +23,12 @@ #include #include +#include + #include #include #include #include -#include -#include -#include #include #include @@ -54,7 +61,18 @@ RunsTableWidget::RunsTableWidget(QWidget *parent) : ui->tblRuns->setInlineEditSaveStrategy(qfw::TableView::OnEditedValueCommit); m_runsTableItemDelegate = new RunsTableItemDelegate(ui->tblRuns); ui->tblRuns->setItemDelegate(m_runsTableItemDelegate); - + auto *event_plugin = getPlugin(); + connect(event_plugin, &EventPlugin::eventOpenChanged, this, [this, event_plugin](bool is_open) { + if (is_open && !event_plugin->eventConfig()->isRelays() && !m_courseItemDelegate) { + m_courseItemDelegate = new CourseItemDelegate(ui->tblRuns); + m_courseItemDelegate->setNullText(tr("Implicit")); + ui->tblRuns->setItemDelegateForColumn(RunsTableModel::col_course_id, m_courseItemDelegate); + } + else if (!is_open && m_courseItemDelegate) { + delete m_courseItemDelegate; + ui->tblRuns->setItemDelegateForColumn(RunsTableModel::col_course_id, nullptr); + } + }); //ui->tblRuns->setSelectionMode(QTableView::SingleSelection); ui->tblRuns->viewport()->setAcceptDrops(true); ui->tblRuns->setDropIndicatorShown(true); @@ -116,6 +134,8 @@ RunsTableWidget::RunsTableWidget(QWidget *parent) : } } }, Qt::QueuedConnection); + + connect(::qx::SqlApi::instance(), &::qx::SqlApi::recchng, this, &RunsTableWidget::onQxRecChng); } RunsTableWidget::~RunsTableWidget() @@ -158,6 +178,9 @@ void RunsTableWidget::reload(int stage_id, int class_id, bool show_offrace, cons ui->lblClassInterval->setText(class_start_interval_min >= 0? QString::number(class_start_interval_min): "---"); } bool is_relays = getPlugin()->eventConfig()->isRelays(); + if (!is_relays && m_courseItemDelegate) { + m_courseItemDelegate->setCourses(definedCourses()); + } auto qb = getPlugin()->runsQuery(stage_id, class_id, show_offrace); qfDebug() << qb.toString(); m_runsTableItemDelegate->setHighlightedClassId(class_id, stage_id); @@ -216,6 +239,17 @@ qf::gui::TableView *RunsTableWidget::tableView() return ui->tblRuns; } +QMap RunsTableWidget::definedCourses() +{ + QMap courses; + qf::core::sql::Query q; + q.exec("SELECT id, name, note FROM courses ORDER BY name, note"); + while(q.next()) { + courses[q.value(0).toInt()] = q.value(1).toString() + ' ' + q.value(2).toString(); + } + return courses; +} + void RunsTableWidget::onCustomContextMenuRequest(const QPoint &pos) { qfLogFuncFrame(); @@ -226,12 +260,14 @@ void RunsTableWidget::onCustomContextMenuRequest(const QPoint &pos) QAction a_shift_start_times(tr("Shift start times in selected rows"), nullptr); QAction a_clear_start_times(tr("Clear start times in selected rows"), nullptr); QAction a_change_class(tr("Set class in selected rows"), nullptr); + QAction a_change_course(tr("Set course in selected rows"), nullptr); QList lst; lst << &a_show_receipt << &a_load_card << &a_print_card << &a_sep1 << &a_shift_start_times << &a_clear_start_times - << &a_change_class; + << &a_change_class + << &a_change_course; QAction *a = QMenu::exec(lst, ui->tblRuns->viewport()->mapToGlobal(pos)); if(a == &a_load_card) { qf::gui::framework::MainWindow *fwk = qf::gui::framework::MainWindow::frameWork(); @@ -344,7 +380,7 @@ void RunsTableWidget::onCustomContextMenuRequest(const QPoint &pos) for(int i : rows) { qf::core::utils::TableRow row = ui->tblRuns->tableRowRef(i); int competitor_id = row.value("competitors.id").toInt(); - q.exec(QString("UPDATE competitors SET classId=%1 WHERE id=%2").arg(class_id).arg(competitor_id), qfc::Exception::Throw); + q.exec(QStringLiteral("UPDATE competitors SET classId=%1 WHERE id=%2").arg(class_id).arg(competitor_id), qfc::Exception::Throw); } transaction.commit(); } @@ -355,6 +391,34 @@ void RunsTableWidget::onCustomContextMenuRequest(const QPoint &pos) ui->tblRuns->reload(true); } } + else if(a == &a_change_course) { + qfw::dialogs::GetItemInputDialog dlg(this); + auto courses = definedCourses(); + QComboBox *box = dlg.comboBox(); + CourseItemDelegate::initCombo(box, courses, CourseItemDelegate::textImplicit()); + dlg.setWindowTitle(tr("Quick Event - Select course")); + dlg.setLabelText(tr("Select course")); + dlg.setCurrentItemIndex(0); + if(dlg.exec()) { + auto course_id = dlg.currentData(); + qfs::Transaction transaction; + try { + QList rows = ui->tblRuns->selectedRowsIndexes(); + qfs::Query q; + for(int i : rows) { + qf::core::utils::TableRow row = ui->tblRuns->tableRowRef(i); + int run_id = row.value("runs.id").toInt(); + auto course_id_str = course_id.isNull() ? QStringLiteral("NULL") : QString::number(course_id.toInt()); + q.exec(QStringLiteral("UPDATE runs SET courseId=%1 WHERE id=%2").arg(course_id_str).arg(run_id), qfc::Exception::Throw); + } + transaction.commit(); + } + catch (std::exception &e) { + qfError() << e.what(); + } + ui->tblRuns->reload(true); + } + } } void RunsTableWidget::onTableViewSqlException(const QString &what, const QString &where, const QString &stack_trace) @@ -373,4 +437,47 @@ void RunsTableWidget::onBadTableDataInput(const QString &message) qf::gui::dialogs::MessageBox::showError(this, message); } +void RunsTableWidget::onQxRecChng(const qf::core::sql::QxRecChng &chng) +{ + std::optional run_id; + int row = 0; + if (chng.table == "competitors") { + auto *m = m_runsModel; + for (auto i=0; irowCount(); ++i) { + if (m->table().row(i).value("competitors.id").toInt() == chng.id) { + run_id = m->value(i, "runs.id").toInt(); + row = i; + break; + } + } + } + else if (chng.table == "runs") { + run_id = chng.id; + auto *m = m_runsModel; + for (auto i=0; irowCount(); ++i) { + if (m->table().row(i).value("runs.id").toInt() == chng.id) { + row = i; + break; + } + } + } + if (run_id.has_value()) { + if (chng.op == qf::core::sql::QxRecOp::Update) { + for (const auto &[k, v] : chng.record.toMap().asKeyValueRange()) { + if (k == "firstname" || k == "lastname") { + auto &r = m_runsModel->tableRowRef(row); + r.setValue("competitors." + k, v); + auto ix = m_runsModel->index(row, RunsTableModel::col_competitorName); + m_runsModel->dataChanged(ix, ix); + } else { + m_runsModel->setValue(row, k, v); + m_runsModel->setDirty(row, k, false); + } + } + } else { + ui->tblRuns->rowExternallySaved(run_id.value()); + } + } +} + diff --git a/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.h b/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.h index 0fd3e80db..d7ed07e6e 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.h +++ b/quickevent/app/quickevent/plugins/Runs/src/runstablewidget.h @@ -4,7 +4,10 @@ class RunsTableModel; class RunsTableItemDelegate; +class CourseItemDelegate; + namespace qf::gui { class TableView; } +namespace qf::core::sql { struct QxRecChng; } namespace Ui { class RunsTableWidget; @@ -28,13 +31,18 @@ class RunsTableWidget : public QWidget Q_SIGNAL void editCompetitorRequest(int competitor_id, int mode); private: + QMap definedCourses(); + void updateStartTimeHighlight() const; void onCustomContextMenuRequest(const QPoint &pos); void onTableViewSqlException(const QString &what, const QString &where, const QString &stack_trace); void onBadTableDataInput(const QString &message); + + void onQxRecChng(const qf::core::sql::QxRecChng &chng); private: Ui::RunsTableWidget *ui; RunsTableModel *m_runsModel; RunsTableItemDelegate *m_runsTableItemDelegate; + CourseItemDelegate *m_courseItemDelegate = nullptr; }; diff --git a/quickevent/app/quickevent/plugins/Runs/src/runswidget.h b/quickevent/app/quickevent/plugins/Runs/src/runswidget.h index 9be9da658..9335be37d 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runswidget.h +++ b/quickevent/app/quickevent/plugins/Runs/src/runswidget.h @@ -11,19 +11,11 @@ class QTextStream; class QLabel; namespace qf { -namespace core { -namespace model { -class SqlTableModel; -} -} -namespace gui { -class ForeignKeyComboBox; -} +namespace core { namespace model { class SqlTableModel; } } +namespace gui { class ForeignKeyComboBox; } } -namespace Event { -class EventPlugin; -} +namespace Event { class EventPlugin; } namespace Ui { class RunsWidget; @@ -69,7 +61,6 @@ class RunsWidget : public QFrame void onDrawRemoveClicked(); void onCbxStageCurrentIndexChanged(); private: - /** * @brief runnersInClubsHistogram * @return list of runs.id for each club sorted by their count, longest list of runners is first diff --git a/quickevent/app/quickevent/src/appversion.h b/quickevent/app/quickevent/src/appversion.h index be3455eff..f8dc06624 100644 --- a/quickevent/app/quickevent/src/appversion.h +++ b/quickevent/app/quickevent/src/appversion.h @@ -1,4 +1,4 @@ #pragma once -#define APP_VERSION "3.4.28" +#define APP_VERSION "3.5.0" diff --git a/quickevent/app/quickevent/src/qx/sqlapi.cpp b/quickevent/app/quickevent/src/qx/sqlapi.cpp new file mode 100644 index 000000000..2fd65b9d8 --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqlapi.cpp @@ -0,0 +1,434 @@ +#include "sqlapi.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace shv::chainpack; + +namespace qx { + +//============================================== +// RpcSqlField +//============================================== +RpcValue RpcSqlField::toRpcValue() const +{ + RpcValue::Map ret; + ret["name"] = name; + return RpcValue(std::move(ret)); +} + +RpcSqlField RpcSqlField::fromRpcValue(const shv::chainpack::RpcValue &rv) +{ + RpcSqlField ret; + const RpcValue::Map &map = rv.asMap(); + ret.name = map.value("name").asString(); + return ret; +} + +//============================================== +// RpcSqlResult +//============================================== +const RpcValue &RpcSqlResult::value(size_t row, size_t col) const +{ + if (row < rows.size()) { + const auto &cells = rows[row]; + if (col < cells.size()) { + return cells[col]; + } + } + static RpcValue s; + return s; +} + +const RpcValue& RpcSqlResult::value(size_t row, const std::string &name) const +{ + if (auto ix = columnIndex(name); ix.has_value()) { + return value(row, ix.value()); + } + static RpcValue s; + return s; +} + +void RpcSqlResult::setValue(size_t row, size_t col, const RpcValue &val) +{ + if (row < rows.size()) { + auto &r = rows[row]; + if (col < r.size()) { + r[col] = val; + } + } +} + +void RpcSqlResult::setValue(size_t row, const std::string &name, const RpcValue &val) +{ + if (auto ix = columnIndex(name); ix.has_value()) { + setValue(row, ix.value(), val); + } +} + +RpcValue::List RpcSqlResult::toRecordList() const +{ + RpcValue::List ret; + for (const auto &row : rows) { + SqlRecord rec; + int n = 0; + for (const auto &field : fields) { + rec[field.name] = row[n++]; + } + ret.push_back(rec); + } + return ret; +} + +std::optional RpcSqlResult::columnIndex(const std::string &name) const +{ + for (size_t col = 0; col < fields.size(); ++col) { + const auto &fld = fields[col]; + if (fld.name == name) { + return col; + } + } + return {}; +} + +RpcValue RpcSqlResult::toRpcValue() const +{ + RpcValue::Map ret; + if(isSelect()) { + RpcValue::List flds; + for(const auto &fld : this->fields) + flds.push_back(fld.toRpcValue()); + ret["fields"] = flds; + ret["rows"] = rows; + } + else { + ret["numRowsAffected"] = numRowsAffected; + ret["lastInsertId"] = lastInsertId.has_value()? RpcValue(lastInsertId.value()): RpcValue(nullptr); + } + return ret; +} + +RpcSqlResult RpcSqlResult::fromRpcValue(const RpcValue &rv) +{ + RpcSqlResult ret; + const auto &map = rv.asMap(); + const auto &flds = map.valref("fields").asList(); + if(flds.empty()) { + ret.numRowsAffected = map.value("numRowsAffected").toInt(); + ret.lastInsertId = map.value("lastInsertId").toInt(); + } + else { + for(const auto &fv : flds) { + ret.fields.push_back(RpcSqlField::fromRpcValue(fv)); + } + for (const auto &row : map.value("rows").asList()) { + ret.rows.push_back(row.asList()); + } + } + return ret; +} + +SqlQueryAndParams SqlQueryAndParams::fromRpcValue(const shv::chainpack::RpcValue &rv) +{ + auto sql_query = rv.asList().valref(0).asString(); + const auto &sql_params = rv.asList().valref(0); + return SqlQueryAndParams { .query = sql_query, .params = sql_params.asMap() }; +} + + +RpcValue qxRecChngToRpcValue(const qf::core::sql::QxRecChng &chng) +{ + RpcValue::Map ret; + ret["table"] = chng.table.toStdString(); + ret["id"] = chng.id; + ret["record"] = shv::coreqt::rpc::qVariantToRpcValue(chng.record); + auto rec_op_string = [](qf::core::sql::QxRecOp op) { + switch (op) { + case qf::core::sql::QxRecOp::Insert: return "Insert"; + case qf::core::sql::QxRecOp::Update: return "Update"; + case qf::core::sql::QxRecOp::Delete: return "Delete"; + } + return ""; + }; + ret["op"] = rec_op_string(chng.op); + return ret; +} + +//============================================== +// SqlApi +//============================================== +SqlApi::SqlApi(QObject *parent) + : QObject{parent} +{ + +} + +SqlApi *SqlApi::instance() +{ + static auto *api = new SqlApi(QCoreApplication::instance()); + return api; +} + +void SqlApi::emitRecChng(const qf::core::sql::QxRecChng &chng) +{ + qfInfo() << "REC_CHNG:" << qxRecChngToRpcValue(chng).toCpon(); + emit recchng(chng); +} + +namespace { + +class Transaction +{ +public: + Transaction(QSqlDatabase db) : m_db(db) { + if (!m_db.transaction()) { + qfWarning() << "BEGIN transaction error:" << m_db.lastError().text(); + throw std::runtime_error("BEGIN transaction error"); + } + } + ~Transaction() { + if (m_inTransaction) { + m_db.rollback(); + } + } + void commit() { + if (!m_db.commit()) { + qfWarning() << "COMMIT transaction error:" << m_db.lastError().text(); + throw std::runtime_error("COMMIT transaction error"); + } + m_inTransaction = false; + } +private: + QSqlDatabase m_db; + bool m_inTransaction = true; +}; + +RpcSqlResult rpcSqlQuery(const SqlQueryAndParams ¶ms) +{ + qf::core::sql::Query q; + q.prepare(QString::fromUtf8(params.query), qf::core::Exception::Throw); + for (const auto &[k, v] : params.params) { + bool ok; + QVariant val = shv::coreqt::rpc::rpcValueToQVariant(v, &ok); + if (!ok) { + QF_EXCEPTION(QStringLiteral("Cannot convert SHV type: %1 to QVariant").arg(v.typeName())); + } + q.bindValue(':' + QString::fromStdString(k), val); + } + q.exec(qf::core::Exception::Throw); + RpcSqlResult ret; + if(q.isSelect()) { + QSqlRecord rec = q.record(); + for (int i = 0; i < rec.count(); ++i) { + QSqlField fld = rec.field(i); + RpcSqlField rfld; + rfld.name = fld.name().toStdString(); + // rfld.name.replace("__", "."); + ret.fields.push_back(rfld); + } + while(q.next()) { + RpcSqlResult::Row row; + for (int i = 0; i < rec.count(); ++i) { + const QVariant v = q.value(i); + if (v.isNull()) { + row.push_back(RpcValue(nullptr)); + } + else { + row.push_back(shv::coreqt::rpc::qVariantToRpcValue(v)); + } + //shvError() << v << v.isNull() << jsv.toVariant() << jsv.toVariant().isNull(); + } + ret.rows.push_back(row); + } + } + else { + ret.numRowsAffected = q.numRowsAffected(); + ret.lastInsertId = q.lastInsertId().toInt(); + } + return ret; +} + +} + +RpcSqlResult SqlApi::exec(const SqlQueryAndParams ¶ms) +{ + return rpcSqlQuery(params); +} + +RpcSqlResult SqlApi::query(const SqlQueryAndParams ¶ms) +{ + return rpcSqlQuery(params); +} + +void SqlApi::transaction(const std::string &query, const shv::chainpack::RpcValue::List ¶ms) +{ + auto conn = qf::core::sql::Connection::forName(); + Transaction tranaction(conn); + qf::core::sql::Query q(conn); + q.prepare(QString::fromUtf8(query), qf::core::Exception::Throw); + for (const auto ¶m : params) { + for (const auto &[k, v] : param.asMap()) { + bool ok; + QVariant val = shv::coreqt::rpc::rpcValueToQVariant(v, &ok); + if (!ok) { + QF_EXCEPTION(QStringLiteral("Cannot convert SHV type: %1 to QVariant").arg(v.typeName())); + } + q.bindValue(':' + QString::fromStdString(k), val); + } + q.exec(qf::core::Exception::Throw); + } + tranaction.commit(); +} + +RpcSqlResult SqlApi::list(const std::string &table, const std::vector &fields, std::optional ids_above, std::optional limit) +{ + QStringList qfields; + for (const auto &fn : fields) { + qfields << QString::fromStdString(fn); + } + if (qfields.isEmpty()) { + qfields << "*"; + } + QString sql_query = QStringLiteral("SELECT %1 FROM %2").arg(qfields.join(',')).arg(QString::fromStdString(table)); + if (ids_above.has_value()) { + sql_query += " WHERE id > " + QString::number(ids_above.value()); + } + if (limit.has_value()) { + sql_query += " LIMIT " + QString::number(limit.value()); + } + auto res = rpcSqlQuery(SqlQueryAndParams { .query = sql_query.toStdString(), .params = {}}); + return res; +} +namespace { +std::string to_lower(const std::string &s) +{ + std::string result; + result.reserve(s.size()); + + std::transform(s.begin(), s.end(), std::back_inserter(result), + [](unsigned char c) { return std::tolower(c); }); + + return result; +} + +SqlRecord normalizeFieldNames(const SqlRecord &rec) +{ + SqlRecord ret; + for (const auto &[k, v] : rec) { + ret[to_lower(k)] = v; + } + return ret; +} +} +int64_t SqlApi::create(const std::string &table, const SqlRecord &record) +{ + QStringList fields; + QStringList placeholders; + for (const auto &[k, v] : record) { + auto name = QString::fromStdString(k); + fields << name; + placeholders << ':' + name; + } + QString sql_query = QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") + .arg(table) + .arg(fields.join(',')) + .arg(placeholders.join(',')); + qf::core::sql::Query q; + q.prepare(sql_query, qf::core::Exception::Throw); + for (const auto &[k, v] : record) { + q.bindValue(':' + QString::fromStdString(k), shv::coreqt::rpc::rpcValueToQVariant(v)); + } + q.exec(qf::core::Exception::Throw); + auto id = q.lastInsertId().toInt(); + SqlApi::instance()->emitRecChng(qf::core::sql::QxRecChng { + .table = QString::fromStdString(table), + .id = id, + .record = shv::coreqt::rpc::rpcValueToQVariant(normalizeFieldNames(record)), + .op = qf::core::sql::QxRecOp::Insert + }); + return id; +} + +std::optional SqlApi::read(const std::string &table, int64_t id, const std::vector &fields) +{ + QStringList qfields; + for (const auto &fn : fields) { + qfields << QString::fromStdString(fn); + } + if (qfields.isEmpty()) { + qfields << "*"; + } + QString sql_query = QStringLiteral("SELECT %1 FROM %2 WHERE id = %3") + .arg(qfields.join(',')) + .arg(QString::fromStdString(table)) + .arg(id) ; + auto res = rpcSqlQuery(SqlQueryAndParams { .query = sql_query.toStdString(), .params = {}}); + auto lst = res.toRecordList(); + if (lst.empty()) { + return {}; + } + return lst[0].asMap(); +} + +bool SqlApi::update(const std::string &table, int64_t id, const SqlRecord &record) +{ + QStringList fields; + for (const auto &[k, v] : record) { + auto name = QString::fromStdString(k); + fields << name + " = :" + name; + } + QString sql_query = QStringLiteral("UPDATE %1 SET %2 WHERE id = %3") + .arg(table) + .arg(fields.join(',')) + .arg(id); + qf::core::sql::Query q; + q.prepare(sql_query, qf::core::Exception::Throw); + for (const auto &[k, v] : record) { + auto qv = shv::coreqt::rpc::rpcValueToQVariant(v); + q.bindValue(':' + QString::fromStdString(k), qv); + } + q.exec(qf::core::Exception::Throw); + bool updated = q.numRowsAffected() == 1; + if (updated) { + SqlApi::instance()->emitRecChng(qf::core::sql::QxRecChng { + .table = QString::fromStdString(table), + .id = id, + .record = shv::coreqt::rpc::rpcValueToQVariant(normalizeFieldNames(record)), + .op = qf::core::sql::QxRecOp::Update + }); + } + return updated; +} + +bool SqlApi::drop(const std::string &table, int64_t id) +{ + QString sql_query = QStringLiteral("DELETE FROM %1 WHERE id = %2") + .arg(table) + .arg(id); + qf::core::sql::Query q; + q.exec(sql_query, qf::core::Exception::Throw); + bool is_drop = q.numRowsAffected() == 1; + if (is_drop) { + SqlApi::instance()->emitRecChng(qf::core::sql::QxRecChng { + .table = QString::fromStdString(table), + .id = id, + .record = {}, + .op = qf::core::sql::QxRecOp::Delete + }); + } + return is_drop; +} + +} diff --git a/quickevent/app/quickevent/src/qx/sqlapi.h b/quickevent/app/quickevent/src/qx/sqlapi.h new file mode 100644 index 000000000..80bee09b2 --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqlapi.h @@ -0,0 +1,81 @@ +#pragma once + +#include +// #include +#include + +#include +#include + +namespace qf::core::sql { struct QxRecChng; } + +namespace qx { + +struct RpcSqlField +{ + std::string name; + + //explicit RpcSqlField(const QJsonObject &jo = QJsonObject()) : Super(jo) {} + shv::chainpack::RpcValue toRpcValue() const; + // QVariant toVariant() const; + static RpcSqlField fromRpcValue(const shv::chainpack::RpcValue &rv); + // static RpcSqlField fromVariant(const QVariant &v); +}; + +struct RpcSqlResult +{ + int numRowsAffected = 0; + std::optional lastInsertId = 0; + std::vector fields; + using Row = shv::chainpack::RpcValue::List; + std::vector rows; + + RpcSqlResult() = default; + + std::optional columnIndex(const std::string &name) const; + const shv::chainpack::RpcValue& value(size_t row, size_t col) const; + const shv::chainpack::RpcValue& value(size_t row, const std::string &name) const; + void setValue(size_t row, size_t col, const shv::chainpack::RpcValue &val); + void setValue(size_t row, const std::string &name, const shv::chainpack::RpcValue &val); + + bool isSelect() const {return !fields.empty();} + shv::chainpack::RpcValue toRpcValue() const; + shv::chainpack::RpcValue::List toRecordList() const; + static RpcSqlResult fromRpcValue(const shv::chainpack::RpcValue &rv); +}; + +using SqlRecord = shv::chainpack::RpcValue::Map; + +struct SqlQueryAndParams +{ + std::string query; + SqlRecord params; + + static SqlQueryAndParams fromRpcValue(const shv::chainpack::RpcValue &rv); +}; + +shv::chainpack::RpcValue qxRecChngToRpcValue(const qf::core::sql::QxRecChng &chng); + + +class SqlApi : public QObject +{ + Q_OBJECT +public: + static SqlApi* instance(); + + Q_SIGNAL void recchng(const qf::core::sql::QxRecChng &chng); + void emitRecChng(const qf::core::sql::QxRecChng &chng); + + static RpcSqlResult exec(const SqlQueryAndParams ¶ms); + static RpcSqlResult query(const SqlQueryAndParams ¶ms); + static void transaction(const std::string &query, const shv::chainpack::RpcValue::List ¶ms); + static RpcSqlResult list(const std::string &table, const std::vector &fields, std::optional ids_above, std::optional limit); + static int64_t create(const std::string &table, const SqlRecord &record); + static std::optional read(const std::string &table, int64_t id, const std::vector &fields); + static bool update(const std::string &table, int64_t id, const SqlRecord &record); + static bool drop(const std::string &table, int64_t id); +private: + explicit SqlApi(QObject *parent = nullptr); +}; + +} diff --git a/quickevent/app/quickevent/src/qx/sqldatadocument.cpp b/quickevent/app/quickevent/src/qx/sqldatadocument.cpp new file mode 100644 index 000000000..1f7040921 --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqldatadocument.cpp @@ -0,0 +1,17 @@ +#include "sqldatadocument.h" + + +namespace qx { + +SqlDataDocument::SqlDataDocument(QObject *parent) + : Super{parent} +{ + +} + +SqlTableModel *SqlDataDocument::createModel(QObject *parent) +{ + return new ::qx::SqlTableModel(parent); +} + +} // namespace qx diff --git a/quickevent/app/quickevent/src/qx/sqldatadocument.h b/quickevent/app/quickevent/src/qx/sqldatadocument.h new file mode 100644 index 000000000..cea298076 --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqldatadocument.h @@ -0,0 +1,21 @@ +#pragma once + +#include "sqltablemodel.h" + +#include + +namespace qx { + +class SqlDataDocument : public qf::gui::model::SqlDataDocument +{ + Q_OBJECT + + using Super = qf::gui::model::SqlDataDocument; +public: + explicit SqlDataDocument(QObject *parent = nullptr); +protected: + ::qx::SqlTableModel* createModel(QObject *parent) override; +}; + +} // namespace qx + diff --git a/quickevent/app/quickevent/src/qx/sqltablemodel.cpp b/quickevent/app/quickevent/src/qx/sqltablemodel.cpp new file mode 100644 index 000000000..0bad5b1fc --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqltablemodel.cpp @@ -0,0 +1,13 @@ +#include "sqltablemodel.h" +#include "sqlapi.h" + +namespace qx { + +SqlTableModel::SqlTableModel(QObject *parent) + : Super{parent} +{ + auto *sql_api = SqlApi::instance(); + connect(this, &qf::gui::model::SqlTableModel::qxRecChng, sql_api, &SqlApi::emitRecChng); +} + +} // namespace qx diff --git a/quickevent/app/quickevent/src/qx/sqltablemodel.h b/quickevent/app/quickevent/src/qx/sqltablemodel.h new file mode 100644 index 000000000..1581be19e --- /dev/null +++ b/quickevent/app/quickevent/src/qx/sqltablemodel.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace qx { + +class SqlTableModel : public quickevent::gui::og::SqlTableModel +{ + Q_OBJECT + + using Super = quickevent::gui::og::SqlTableModel; +public: + explicit SqlTableModel(QObject *parent = nullptr); +}; + +} // namespace qx +