]> Some of my projects - localmylist.git/commitdiff
Add new version of DynamicModel.
authorAPTX <marek321@gmail.com>
Sun, 25 Jan 2015 11:12:12 +0000 (12:12 +0100)
committerAPTX <marek321@gmail.com>
Sun, 25 Jan 2015 11:45:50 +0000 (12:45 +0100)
The intention is to replace MyListModel with DynamicModel completely in the
future. Currently MyListModel still does some things DynamicModel can't do,
but it should be a replacement for most use cases.

DynamicModel reacts to changes in the database (if built with Qt5) including
deletions.

New features:
* Filter Anime by alternate titles (if anime is the first level)
* Special episode counts are separated from normal episode counts

30 files changed:
localmylist-management/dynamicmodelfiltermodel.cpp [new file with mode: 0644]
localmylist-management/dynamicmodelfiltermodel.h [new file with mode: 0644]
localmylist-management/dynamicmodelitemdelegate.cpp [new file with mode: 0644]
localmylist-management/dynamicmodelitemdelegate.h [new file with mode: 0644]
localmylist-management/dynamicmodelview.cpp [new file with mode: 0644]
localmylist-management/dynamicmodelview.h [new file with mode: 0644]
localmylist-management/localmylist-management.pro
localmylist-management/tabs/dynamicmodeltab.cpp
localmylist-management/tabs/dynamicmodeltab.h
localmylist-management/tabs/dynamicmodeltab.ui
localmylist/database.cpp
localmylist/database.h
localmylist/dynamicmodel/data.cpp
localmylist/dynamicmodel/data.h
localmylist/dynamicmodel/datamodel.cpp
localmylist/dynamicmodel/datamodel.h
localmylist/dynamicmodel/datatype.cpp
localmylist/dynamicmodel/datatype.h
localmylist/dynamicmodel/model.cpp
localmylist/dynamicmodel/model.h
localmylist/dynamicmodel/node.cpp
localmylist/dynamicmodel/node.h
localmylist/dynamicmodel/queryparser.cpp [new file with mode: 0644]
localmylist/dynamicmodel/queryparser.h [new file with mode: 0644]
localmylist/dynamicmodel/typerelation.cpp
localmylist/dynamicmodel/typerelation.h
localmylist/dynamicmodel/types.cpp
localmylist/dynamicmodel/types.h
localmylist/localmylist.pro
localmylist/share/schema/schema.sql

diff --git a/localmylist-management/dynamicmodelfiltermodel.cpp b/localmylist-management/dynamicmodelfiltermodel.cpp
new file mode 100644 (file)
index 0000000..54c198b
--- /dev/null
@@ -0,0 +1,42 @@
+#include "dynamicmodelfiltermodel.h"\r
+\r
+#include "mylist.h"\r
+#include "settings.h"\r
+#include "dynamicmodel/model.h"\r
+#include "dynamicmodel/node.h"\r
+\r
+#include <QDebug>\r
+\r
+DynamicModelFilterModel::DynamicModelFilterModel(QObject *parent) :\r
+       QSortFilterProxyModel(parent)\r
+{\r
+       setFilterCaseSensitivity(Qt::CaseInsensitive);\r
+\r
+       connect(LocalMyList::instance()->database(), SIGNAL(configChanged()), this, SLOT(configChanged()));\r
+}\r
+\r
+LocalMyList::DynamicModel::Model *DynamicModelFilterModel::dynamicModel() const\r
+{\r
+       return qobject_cast<LocalMyList::DynamicModel::Model *>(sourceModel());\r
+}\r
+\r
+LocalMyList::DynamicModel::Node *DynamicModelFilterModel::node(const QModelIndex &idx) const\r
+{\r
+       if (!idx.isValid())\r
+               return 0;\r
+\r
+       return dynamicModel()->node(mapToSource(idx));\r
+}\r
+\r
+\r
+bool DynamicModelFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const\r
+{\r
+       const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);\r
+\r
+       if (!source_parent.isValid())\r
+       {\r
+               return dynamicModel()->node(idx)->data()->matchesFilter(filterRegExp());\r
+       }\r
+\r
+       return true;\r
+}\r
diff --git a/localmylist-management/dynamicmodelfiltermodel.h b/localmylist-management/dynamicmodelfiltermodel.h
new file mode 100644 (file)
index 0000000..ceb23e0
--- /dev/null
@@ -0,0 +1,28 @@
+#ifndef DYNAMICMODELFILTERMODEL_H\r
+#define DYNAMICMODELFILTERMODEL_H\r
+\r
+#include <QSortFilterProxyModel>\r
+\r
+namespace LocalMyList {\r
+namespace DynamicModel {\r
+class Model;\r
+class Node;\r
+}\r
+}\r
+\r
+class DynamicModelFilterModel : public QSortFilterProxyModel\r
+{\r
+       Q_OBJECT\r
+\r
+public:\r
+       explicit DynamicModelFilterModel(QObject *parent = 0);\r
+\r
+public slots:\r
+       LocalMyList::DynamicModel::Model *dynamicModel() const;\r
+       LocalMyList::DynamicModel::Node *node(const QModelIndex &idx) const;\r
+\r
+protected:\r
+       bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;\r
+};\r
+\r
+#endif // DYNAMICMODELFILTERMODEL_H\r
diff --git a/localmylist-management/dynamicmodelitemdelegate.cpp b/localmylist-management/dynamicmodelitemdelegate.cpp
new file mode 100644 (file)
index 0000000..646d7d0
--- /dev/null
@@ -0,0 +1,63 @@
+#include "dynamicmodelitemdelegate.h"\r
+\r
+#include <QDoubleSpinBox>\r
+\r
+#include "dynamicmodel/data.h"\r
+#include "dynamicmodel/node.h"\r
+#include "dynamicmodelfiltermodel.h"\r
+\r
+DynamicModelItemDelegate::DynamicModelItemDelegate(QObject *parent) :\r
+       QStyledItemDelegate(parent)\r
+{\r
+}\r
+\r
+QWidget *DynamicModelItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       if (!isVoteField(index))\r
+               return QStyledItemDelegate::createEditor(parent, option, index);\r
+\r
+       QDoubleSpinBox *ed = new QDoubleSpinBox(parent);\r
+\r
+       ed->setRange(0.99, 10.00);\r
+       ed->setDecimals(2);\r
+       ed->setSingleStep(0.50);\r
+       ed->setSpecialValueText(tr("No Vote/Revoke"));\r
+       return ed;\r
+}\r
+\r
+void DynamicModelItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const\r
+{\r
+       if (!isVoteField(index))\r
+               return QStyledItemDelegate::setEditorData(editor, index);\r
+\r
+       double vote = index.data(Qt::EditRole).toDouble();\r
+\r
+       if (vote < 1.00 || vote > 10.00)\r
+               vote = 5.00;\r
+\r
+       QDoubleSpinBox *ed = qobject_cast<QDoubleSpinBox *>(editor);\r
+       ed->setValue(vote);\r
+}\r
+\r
+void DynamicModelItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const\r
+{\r
+       if (!isVoteField(index))\r
+               QStyledItemDelegate::setModelData(editor, model, index);\r
+\r
+       QDoubleSpinBox *ed = qobject_cast<QDoubleSpinBox *>(editor);\r
+       model->setData(index, ed->value());\r
+}\r
+\r
+bool DynamicModelItemDelegate::isVoteField(const QModelIndex &index) const\r
+{\r
+       using namespace LocalMyList;\r
+       const DynamicModelFilterModel *model = qobject_cast<const DynamicModelFilterModel *>(index.model());\r
+       const DynamicModel::Node *node = model->node(index);\r
+\r
+       if (!node->data())\r
+               return false;\r
+\r
+       return node->data()->isVoteColumn(index.column());\r
+}\r
diff --git a/localmylist-management/dynamicmodelitemdelegate.h b/localmylist-management/dynamicmodelitemdelegate.h
new file mode 100644 (file)
index 0000000..616a9f8
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef DYNAMICMODELITEMDELEGATE_H\r
+#define DYNAMICMODELITEMDELEGATE_H\r
+\r
+#include <QStyledItemDelegate>\r
+\r
+class DynamicModelItemDelegate : public QStyledItemDelegate\r
+{\r
+       Q_OBJECT\r
+public:\r
+       explicit DynamicModelItemDelegate(QObject *parent = 0);\r
+\r
+       QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const;\r
+       void setEditorData(QWidget *editor, const QModelIndex &index ) const;\r
+       void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;\r
+\r
+private:\r
+       bool isVoteField(const QModelIndex &index) const;\r
+\r
+};\r
+\r
+#endif // DYNAMICMODELITEMDELEGATE_H\r
diff --git a/localmylist-management/dynamicmodelview.cpp b/localmylist-management/dynamicmodelview.cpp
new file mode 100644 (file)
index 0000000..b108a20
--- /dev/null
@@ -0,0 +1,260 @@
+#include "dynamicmodelview.h"\r
+#include "mylist.h"\r
+#include "database.h"\r
+#include "dynamicmodel/model.h"\r
+#include "dynamicmodel/node.h"\r
+#include "dynamicmodel/datatype.h"\r
+#include "dynamicmodel/data.h"\r
+\r
+#include "dynamicmodelfiltermodel.h"\r
+\r
+#include <QHeaderView>\r
+#include <QMenu>\r
+#include <QDesktopServices>\r
+#include <QUrl>\r
+#include <QKeyEvent>\r
+\r
+DynamicModelView::DynamicModelView(QWidget *parent) :\r
+       QTreeView(parent)\r
+{\r
+       setContextMenuPolicy(Qt::CustomContextMenu);\r
+       connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showCustomContextMenu(QPoint)));\r
+\r
+       this->setExpandsOnDoubleClick(false);\r
+       connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClick(QModelIndex)));\r
+\r
+       openAction = new QAction(tr("Open"), this);\r
+       connect(openAction, SIGNAL(triggered()), this, SLOT(requestOpenFile()));\r
+       openNextAction = new QAction(tr("Open Next"), this);\r
+       connect(openNextAction, SIGNAL(triggered()), this, SLOT(requestOpenFile()));\r
+       markAnimeWatchedAction = new QAction(tr("Mark Anime Watched"), this);\r
+       connect(markAnimeWatchedAction, SIGNAL(triggered()), this, SLOT(markAnimeWatched()));\r
+       markEpisodeWatchedAction = new QAction(tr("Mark Episode Watched"), this);\r
+       connect(markEpisodeWatchedAction, SIGNAL(triggered()), this, SLOT(markEpisodeWatched()));\r
+       markFileWatchedAction = new QAction(tr("Mark Watched"), this);\r
+       connect(markFileWatchedAction, SIGNAL(triggered()), this, SLOT(markFileWatched()));\r
+       markFileUnwatchedAction = new QAction(tr("Mark Unwatched"), this);\r
+       connect(markFileUnwatchedAction, SIGNAL(triggered()), this, SLOT(markFileUnwatched()));\r
+       aniDBLinkAction = new QAction(tr("Open AniDB Page"), this);\r
+       connect(aniDBLinkAction, SIGNAL(triggered()), this, SLOT(openAnidbPage()));\r
+       renameFilesAction = new QAction(tr("Rename Files Related to Entry"), this);\r
+       connect(renameFilesAction, SIGNAL(triggered()), this, SLOT(requestFileRename()));\r
+       renameTestAction = new QAction(tr("Use For Rename Testing"), this);\r
+       connect(renameTestAction, SIGNAL(triggered()), this, SLOT(renameTest()));\r
+       requestDataAction = new QAction(tr("Request Data for this Entry"), this);\r
+       connect(requestDataAction, SIGNAL(triggered()), this, SLOT(requestData()));\r
+       removeFileLocationAction = new QAction(tr("Remove this File Location"), this);\r
+       connect(removeFileLocationAction, SIGNAL(triggered()), this, SLOT(removeFileLocation()));\r
+\r
+       if (!LocalMyList::MyList::isUdpClientAvailable())\r
+       {\r
+               renameFilesAction->setDisabled(true);\r
+               renameTestAction->setDisabled(true);\r
+       }\r
+}\r
+\r
+void DynamicModelView::keyPressEvent(QKeyEvent *event)\r
+{\r
+       if (event->key() == Qt::Key_Return && currentIndex().isValid())\r
+       {\r
+               emit openFileRequested(currentIndex());\r
+               event->accept();\r
+       }\r
+       else\r
+       {\r
+               QTreeView::keyPressEvent(event);\r
+       }\r
+}\r
+\r
+DynamicModelFilterModel *DynamicModelView::dynamicModelFilterModel() const\r
+{\r
+       return qobject_cast<DynamicModelFilterModel *>(model());\r
+}\r
+\r
+void DynamicModelView::showCustomContextMenu(const QPoint &pos)\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       const QModelIndex idx = indexAt(pos);\r
+       if (!idx.isValid())\r
+               return;\r
+\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(idx);\r
+\r
+       QList<QAction *> actions;\r
+\r
+       if (node->data()->type()->name() == "anime")\r
+       {\r
+               aniDBLinkAction->setText(tr("Open AniDB Page (%1%2)").arg('a').arg(node->id()));\r
+               actions << aniDBLinkAction\r
+                               << openNextAction\r
+                               << markAnimeWatchedAction\r
+                               << renameFilesAction\r
+                               << requestDataAction;\r
+       }\r
+       else if (node->data()->type()->name() == "episode")\r
+       {\r
+               aniDBLinkAction->setText(tr("Open AniDB Page (%1%2)").arg('e').arg(node->id()));\r
+               actions << aniDBLinkAction\r
+                               << openAction\r
+                               << markEpisodeWatchedAction\r
+                               << renameFilesAction\r
+                               << requestDataAction;\r
+       }\r
+       else if (node->data()->type()->name() == "file")\r
+       {\r
+               aniDBLinkAction->setText(tr("Open AniDB Page (%1%2)").arg('f').arg(node->id()));\r
+               actions << aniDBLinkAction\r
+                               << openAction\r
+                               << markFileWatchedAction\r
+                               << markFileUnwatchedAction\r
+                               << renameTestAction\r
+                               << renameFilesAction\r
+                               << requestDataAction;\r
+       }\r
+       else if (node->data()->type()->name() == "file_location")\r
+       {\r
+               aniDBLinkAction->setText(tr("Open AniDB Page (%1%2) (%3%4)")\r
+                                                                .arg('f').arg(node->parent()->id())\r
+                                                                .arg("LocationId").arg(node->id()));\r
+               actions << aniDBLinkAction\r
+                               << renameTestAction\r
+                               << renameFilesAction\r
+                               << removeFileLocationAction;\r
+       }\r
+\r
+       if(actions.isEmpty())\r
+               return;\r
+\r
+       customContextMenuIndex = idx;\r
+       QMenu::exec(actions, viewport()->mapToGlobal(pos));\r
+       customContextMenuIndex = QModelIndex();\r
+}\r
+\r
+void DynamicModelView::doubleClick(const QModelIndex &index)\r
+{\r
+       if (!(model()->flags(index) & Qt::ItemIsEditable))\r
+               emit openFileRequested(index);\r
+}\r
+\r
+void DynamicModelView::requestOpenFile()\r
+{\r
+       emit openFileRequested(customContextMenuIndex);\r
+}\r
+\r
+void DynamicModelView::markAnimeWatched()\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+\r
+       if (node->data()->type()->name() != "anime")\r
+               return;\r
+\r
+       PendingMyListUpdate pmu;\r
+       pmu.aid = node->id();\r
+\r
+       pmu.setMyWatched = true;\r
+       pmu.myWatched = QDateTime::currentDateTime();\r
+\r
+       MyList::instance()->database()->addPendingMyListUpdate(pmu);\r
+}\r
+\r
+void DynamicModelView::markEpisodeWatched()\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+\r
+       if (node->data()->type()->name() != "episode")\r
+               return;\r
+\r
+       const auto data = static_cast<DynamicModel::EpisodeData *>(node->data());\r
+\r
+       PendingMyListUpdate pmu;\r
+       pmu.aid = data->episodeData.aid;\r
+       pmu.epno = data->episodeData.epno;\r
+       pmu.eptype = data->episodeData.type;\r
+\r
+       pmu.setMyWatched = true;\r
+       pmu.myWatched = QDateTime::currentDateTime();\r
+\r
+       MyList::instance()->database()->addPendingMyListUpdate(pmu);\r
+}\r
+\r
+void DynamicModelView::markFileWatched()\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+\r
+       if (node->data()->type()->name() != "file")\r
+               return;\r
+\r
+       MyList::instance()->markWatched(node->id());\r
+}\r
+\r
+void DynamicModelView::markFileUnwatched()\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+\r
+       if (node->data()->type()->name() != "file")\r
+               return;\r
+\r
+       MyList::instance()->markUnwatched(node->id());\r
+}\r
+\r
+void DynamicModelView::openAnidbPage()\r
+{\r
+       using namespace LocalMyList;\r
+\r
+       static const QString aniDBUrlBase = "http://anidb.net/%1%2";\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+\r
+       if (node->data()->type()->name() == "anime")\r
+               QDesktopServices::openUrl(QUrl(aniDBUrlBase.arg('a').arg(node->id())));\r
+       else if (node->data()->type()->name() == "episode")\r
+               QDesktopServices::openUrl(QUrl(aniDBUrlBase.arg('e').arg(node->id())));\r
+       else if (node->data()->type()->name() == "file")\r
+               QDesktopServices::openUrl(QUrl(aniDBUrlBase.arg('f').arg(node->id())));\r
+       else if (node->data()->type()->name() == "file_location")\r
+               QDesktopServices::openUrl(QUrl(aniDBUrlBase.arg('f').arg(node->parent()->id())));\r
+}\r
+\r
+void DynamicModelView::requestFileRename()\r
+{\r
+       emit renameFilesRequested(customContextMenuIndex);\r
+}\r
+\r
+void DynamicModelView::renameTest()\r
+{\r
+       using namespace LocalMyList;\r
+       int id;\r
+       DynamicModel::Node *node = dynamicModelFilterModel()->node(customContextMenuIndex);\r
+       if (node->data()->type()->name() == "file")\r
+       {\r
+               id = node->id();\r
+       }\r
+       else if (node->data()->type()->name() == "file_location")\r
+       {\r
+               const auto data = static_cast<DynamicModel::FileLocationData *>(node->data());\r
+               id = data->fileLocationData.fid;\r
+       }\r
+\r
+       if (id)\r
+               emit renameTest(id);\r
+}\r
+\r
+void DynamicModelView::requestData()\r
+{\r
+       emit dataRequested(customContextMenuIndex);\r
+}\r
+\r
+void DynamicModelView::removeFileLocation()\r
+{\r
+       int id = dynamicModelFilterModel()->node(customContextMenuIndex)->id();\r
+       if (id)\r
+               emit removeFileLocationRequested(id);\r
+}\r
diff --git a/localmylist-management/dynamicmodelview.h b/localmylist-management/dynamicmodelview.h
new file mode 100644 (file)
index 0000000..ccaab98
--- /dev/null
@@ -0,0 +1,63 @@
+#ifndef DYNAMICMODELVIEW_H\r
+#define DYNAMICMODELVIEW_H\r
+\r
+#include <QTreeView>\r
+\r
+namespace LocalMyList {\r
+namespace DynamicModel {\r
+class Model;\r
+class Node;\r
+}\r
+}\r
+\r
+class DynamicModelFilterModel;\r
+\r
+class DynamicModelView : public QTreeView\r
+{\r
+       Q_OBJECT\r
+public:\r
+       explicit DynamicModelView(QWidget *parent = 0);\r
+\r
+protected:\r
+       void keyPressEvent(QKeyEvent *event);\r
+\r
+signals:\r
+       void openFileRequested(const QModelIndex &index);\r
+       void renameFilesRequested(const QModelIndex &index);\r
+       void dataRequested(const QModelIndex &index);\r
+       void renameTest(int fid);\r
+       void removeFileLocationRequested(int locationId);\r
+\r
+private slots:\r
+       DynamicModelFilterModel *dynamicModelFilterModel() const;\r
+       void showCustomContextMenu(const QPoint &pos);\r
+       void doubleClick(const QModelIndex &index);\r
+       void requestOpenFile();\r
+       void markAnimeWatched();\r
+       void markEpisodeWatched();\r
+       void markFileWatched();\r
+       void markFileUnwatched();\r
+       void openAnidbPage();\r
+       void requestFileRename();\r
+       void renameTest();\r
+       void requestData();\r
+       void removeFileLocation();\r
+\r
+private:\r
+       QModelIndex customContextMenuIndex;\r
+\r
+\r
+       QAction *openAction;\r
+       QAction *openNextAction;\r
+       QAction *markAnimeWatchedAction;\r
+       QAction *markEpisodeWatchedAction;\r
+       QAction *markFileWatchedAction;\r
+       QAction *markFileUnwatchedAction;\r
+       QAction *aniDBLinkAction;\r
+       QAction *renameTestAction;\r
+       QAction *renameFilesAction;\r
+       QAction *requestDataAction;\r
+       QAction *removeFileLocationAction;\r
+};\r
+\r
+#endif // DYNAMICMODELVIEW_H\r
index f3a1948ebf9dd88be3696a02b917ab7d732db94b..a79e8b9f56ac451af23456360281399f1f1c9946 100644 (file)
@@ -32,7 +32,12 @@ SOURCES += main.cpp\
        aniaddsyntaxhighlighter.cpp \
        settingsdialog.cpp \
        codeeditor.cpp \
-    tabs/dynamicmodeltab.cpp
+       tabs/dynamicmodeltab.cpp \
+       dynamicmodelfiltermodel.cpp \
+       dynamicmodelview.cpp \
+       dynamicmodelitemdelegate.cpp \
+       setupwizard.cpp \
+       commandline.cpp
 
 HEADERS += mainwindow.h \
        databaseconnectiondialog.h \
@@ -55,7 +60,12 @@ HEADERS += mainwindow.h \
        aniaddsyntaxhighlighter.h \
        settingsdialog.h \
        codeeditor.h \
-    tabs/dynamicmodeltab.h
+       tabs/dynamicmodeltab.h \
+       dynamicmodelfiltermodel.h \
+       dynamicmodelview.h \
+       dynamicmodelitemdelegate.h \
+       setupwizard.h \
+       commandline.h
 
 FORMS += mainwindow.ui \
        databaseconnectiondialog.ui \
@@ -67,7 +77,7 @@ FORMS += mainwindow.ui \
        tabs/pendingrequesttab.ui \
        tabs/databaselogtab.ui \
        tabs/clientlogtab.ui \
-    tabs/dynamicmodeltab.ui
+       tabs/dynamicmodeltab.ui
 
 include(../localmylist.pri)
 include(qtsingleapplication/qtsingleapplication.pri)
@@ -80,5 +90,11 @@ include(qtsingleapplication/qtsingleapplication.pri)
 } else {
        DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT
 }
+
+# Why is this required with Qt5.4?
+win32 {
+       LIBS += -ladvapi32 -lshell32
+}
+
 target.path = $${PREFIX}/bin
 INSTALLS += target
index 493c4e6dc536c88c1bc95f0a2f1e3fb49d60d85b..85033896f83fa9456d65a42d55b269bff21731d8 100644 (file)
@@ -1,17 +1,25 @@
 #include "dynamicmodeltab.h"
 #include "ui_dynamicmodeltab.h"
 
+#include <QMessageBox>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QSqlError>
+
 #include "mainwindow.h"
 #include "database.h"
 #include "mylist.h"
-#include "mylistfiltermodel.h"
-#include "mylistitemdelegate.h"
+#include "dynamicmodelfiltermodel.h"
+#include "dynamicmodelitemdelegate.h"
 
 #include "dynamicmodel/model.h"
 #include "dynamicmodel/datamodel.h"
 #include "dynamicmodel/types.h"
 #include "dynamicmodel/typerelation.h"
 
+#include <QDebug>
+
+using namespace LocalMyList;
 using namespace LocalMyList::DynamicModel;
 
 DynamicModelTab::DynamicModelTab(QWidget *parent) :
@@ -42,29 +50,30 @@ QString DynamicModelTab::name()
 
 void DynamicModelTab::init()
 {
+       // Model must be deleted before the DataModel is uses.
+       model = new Model(this);
+
+       // TODO: move outside the tab as it should be useful globally
        dataModel = new DataModel(this);
+       dataModel->registerDataType(new ColumnType);
        dataModel->registerDataType(new AnimeType);
        dataModel->registerDataType(new EpisodeType);
        dataModel->registerDataType(new FileType);
        dataModel->registerDataType(new FileLocationType);
        dataModel->registerDataType(new AnimeTitleType);
-       dataModel->registerTypeRelation(new RootAnimeRelation(this));
-       dataModel->registerTypeRelation(new RootEpisodeRelation(this));
-       dataModel->registerTypeRelation(new AnimeEpisodeRelation(this));
-       dataModel->registerTypeRelation(new EpisodeFileRelation(this));
-       dataModel->registerTypeRelation(new FileFileLocationRelation(this));
-       dataModel->registerTypeRelation(new RootAnimeTitleRelation(this));
-       dataModel->registerTypeRelation(new AnimeTitleAnimeRelation(this));
-       dataModel->registerTypeRelation(new AnimeTitleEpisodeRelation(this));
-       dataModel->registerTypeRelation(new AnimeAnimeTitleRelation(this));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("anime", "episode", "aid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("episode", "anime", "aid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("anime", "file", "aid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("file", "anime", "aid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("episode", "file", "eid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("file", "episode", "eid"));
+       dataModel->registerTypeRelation(new ForeignKeyRelation("file", "file_location", "fid"));
 
-       model = new Model(this);
-       model->setDataModel(dataModel);
 
-       myListFilterModel = new MyListFilterModel(this);
-       myListFilterModel->setSourceModel(model);
-       ui->myListView->setModel(myListFilterModel);
-       ui->myListView->setItemDelegate(new MyListItemDelegate(ui->myListView));
+       dynamicModelFilterModel = new DynamicModelFilterModel(this);
+       dynamicModelFilterModel->setSourceModel(model);
+       ui->myListView->setModel(dynamicModelFilterModel);
+       ui->myListView->setItemDelegate(new DynamicModelItemDelegate(ui->myListView));
 
 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
        ui->myListView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
@@ -80,12 +89,16 @@ void DynamicModelTab::init()
                                                         << tr("Regexp"));
 
        connect(ui->myListView, SIGNAL(renameTest(int)), mainWindow(), SLOT(openRenameScriptEditor(int)));
-       connect(ui->myListView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(currentSelectionChanged(QModelIndex,QModelIndex)));
-       connect(ui->filterInput, SIGNAL(textChanged(QString)), this, SLOT(currentSelectionChanged()));
 
        connect(model, SIGNAL(queryChanged(QString)), ui->modelQuery, SLOT(setText(QString)));
-       //model->setQuery("anime|episode|file|file_location");
-       model->setQuery("anime|episode");
+       QueryParser q(dataModel);
+       q.parse("...");
+
+       if (!q.isValid()) {
+               qDebug() << "Invalid query" << q.errorString();
+       }
+
+       model->setQuery(q);
 }
 
 void DynamicModelTab::activate()
@@ -110,19 +123,148 @@ void DynamicModelTab::changeEvent(QEvent *e)
        }
 }
 
+
+void DynamicModelTab::on_myListView_openFileRequested(const QModelIndex &index)
+{
+       DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+       if (!node->id())
+               return;
+
+       OpenFileData data;
+
+       if (node->data()->type()->name() == "anime")
+       {
+               data = MyList::instance()->database()->firstUnwatchedByAid(node->id());
+       }
+       else if (node->data()->type()->name() == "episode")
+       {
+               data = MyList::instance()->database()->openFileByEid(node->id());
+       }
+       else if (node->data()->type()->name() == "file")
+       {
+               data = MyList::instance()->database()->openFile(node->id());
+       }
+       else
+       {
+               return;
+       }
+
+       if (!data.fid)
+       {
+               mainWindow()->showMessage(tr("No file found."));
+               return;
+       }
+
+       QDesktopServices::openUrl(QUrl("file:///" + data.path, QUrl::TolerantMode));
+       mainWindow()->showMessage(tr("Openieng file: %1").arg(data.path));
+
+}
+
+void DynamicModelTab::on_myListView_renameFilesRequested(const QModelIndex &index)
+{
+       DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+       if (!node->id())
+               return;
+
+       QString path;
+       QSqlQuery q(MyList::instance()->database()->connection());
+
+       QChar typeLetter;
+       if (node->data()->type()->name() == "anime")
+       {
+               q.prepare(
+                       "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+                       "       FROM file f "
+                       "       WHERE f.fid = fl.fid AND f.aid = :aid");
+               q.bindValue(":aid", node->id());
+
+               typeLetter = 'a';
+       }
+       else if (node->data()->type()->name() == "episode")
+       {
+               q.prepare(
+                       "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+                       "       FROM file f "
+                       "       WHERE f.fid = fl.fid AND f.eid = :eid");
+               q.bindValue(":eid", node->id());
+
+               typeLetter = 'e';
+       }
+       else if (node->data()->type()->name() == "file")
+       {
+               q.prepare(
+                       "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+                       "       WHERE fl.fid = :fid");
+               q.bindValue(":fid", node->id());
+
+               typeLetter = 'f';
+       }
+       else if (node->data()->type()->name() == "file_location")
+       {
+               q.prepare(
+                       "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+                       "       WHERE fl.location_id = :locationId");
+               q.bindValue(":locationId", node->id());
+
+               typeLetter = 'l';
+       }
+       else
+       {
+               return;
+       }
+
+       if (!q.exec())
+       {
+               qDebug() << q.lastError();
+               return;
+       }
+
+       mainWindow()->showMessage(tr("Files for %1%2 scheduled for rename").arg(typeLetter).arg(node->id()));
+}
+
+void DynamicModelTab::on_myListView_dataRequested(const QModelIndex &index)
+{
+       DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+       if (!node->id())
+               return;
+
+       PendingRequest r;
+
+       if (node->data()->type()->name() == "anime")
+               r.aid = node->id();
+       else if (node->data()->type()->name() == "episode")
+               r.eid = node->id();
+       else if (node->data()->type()->name() == "file")
+               r.fid = node->id();
+       else
+               return;
+
+       MyList::instance()->database()->addRequest(r);
+}
+
+void DynamicModelTab::on_myListView_removeFileLocationRequested(int id)
+{
+       Q_UNUSED(id);
+       //myListModel()->removeFileLocation(id);
+}
+
+
 void DynamicModelTab::on_filterInput_textChanged(const QString &filter)
 {
        switch (ui->filterType->currentIndex())
        {
                case 1:
-                       myListFilterModel->setFilterWildcard(filter);
+                       dynamicModelFilterModel->setFilterWildcard(filter);
                break;
                case 2:
-                       myListFilterModel->setFilterRegExp(filter);
+                       dynamicModelFilterModel->setFilterRegExp(filter);
                break;
                case 0:
                default:
-                       myListFilterModel->setFilterFixedString(filter);
+                       dynamicModelFilterModel->setFilterFixedString(filter);
                break;
        }
 }
@@ -134,61 +276,62 @@ void DynamicModelTab::on_filterType_currentIndexChanged(int)
 
 void DynamicModelTab::on_filterInput_keyUpPressed()
 {
-       selectedRow = qMax(-1, selectedRow - 1);
-       updateSelection();
+       const int rowCount{ui->myListView->model()->rowCount()};
 
-}
+       if (!rowCount)
+               return;
 
-void DynamicModelTab::on_filterInput_keyDownPressed()
-{
-       int newSelectedRow = qMin(model->rowCount() - 1, selectedRow + 1);
+       const QModelIndex currentIdx{ui->myListView->selectionModel()->currentIndex()};
+       QModelIndex nextIdx{ui->myListView->model()->index(currentIdx.row() - 1, 0)};
 
-       if (selectedRow == newSelectedRow)
-               return;
+       if (!nextIdx.isValid())
+               nextIdx = ui->myListView->model()->index(rowCount - 1, 0);
 
-       selectedRow = newSelectedRow;
-       updateSelection();
+       ui->myListView->selectionModel()->
+                       setCurrentIndex(nextIdx, QItemSelectionModel::ClearAndSelect
+                                                       | QItemSelectionModel::Rows);
 }
 
-void DynamicModelTab::on_filterInput_returnPressed()
+void DynamicModelTab::on_filterInput_keyDownPressed()
 {
-       if (selectedRow < 0)
+       if (!ui->myListView->model()->rowCount())
                return;
 
-       const QModelIndex idx = myListFilterModel->index(selectedRow, 0);
-//     on_myListView_openFileRequested(idx);
-}
+       const QModelIndex currentIdx{ui->myListView->selectionModel()->currentIndex()};
+       QModelIndex nextIdx{ui->myListView->model()->index(currentIdx.row() + 1, 0)};
 
-void DynamicModelTab::currentSelectionChanged(const QModelIndex &current, const QModelIndex &)
-{
-       selectedRow = current.row();
-}
+       if (!nextIdx.isValid())
+               nextIdx = ui->myListView->model()->index(0, 0);
 
-void DynamicModelTab::currentSelectionChanged()
-{
-       selectedRow = -1;
+       ui->myListView->selectionModel()->
+                       setCurrentIndex(nextIdx, QItemSelectionModel::ClearAndSelect
+                                                       | QItemSelectionModel::Rows);
 }
 
-void DynamicModelTab::updateSelection()
+void DynamicModelTab::on_filterInput_returnPressed()
 {
-       if (selectedRow < 0)
-       {
-               ui->myListView->selectionModel()->clear();
+       const QModelIndex idx{ui->myListView->selectionModel()->currentIndex()};
+
+       if (!idx.isValid())
                return;
-       }
 
-       const QModelIndex idx = myListFilterModel->index(selectedRow, 0);
-       ui->myListView->selectionModel()->
-                       setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect
-                                                       | QItemSelectionModel::Rows);
+       on_myListView_openFileRequested(idx);
 }
 
 void DynamicModelTab::on_modelQuery_returnPressed()
 {
-       model->setQuery(ui->modelQuery->text());
+       QueryParser q(dataModel);
+       if (q.parse(ui->modelQuery->text()))
+       {
+               model->setQuery(q);
+       }
+       else
+       {
+               QMessageBox::critical(this, tr("Query parse error"), q.errorString());
+       }
 }
 
 void DynamicModelTab::on_modelQueryButton_clicked()
 {
-       model->setQuery(ui->modelQuery->text());
+       on_modelQuery_returnPressed();
 }
index 9d4731c1ffb39b8c9ac2836945a894b391379ca4..0f3c17632baef648ecf517e59078e8c7dbc74f1b 100644 (file)
@@ -4,7 +4,7 @@
 #include "abstracttab.h"
 #include <QModelIndex>
 
-class MyListFilterModel;
+class DynamicModelFilterModel;
 
 namespace LocalMyList {
 namespace DynamicModel {
@@ -37,6 +37,11 @@ protected:
        void changeEvent(QEvent *e);
 
 private slots:
+       void on_myListView_openFileRequested(const QModelIndex &index);
+       void on_myListView_renameFilesRequested(const QModelIndex &index);
+       void on_myListView_dataRequested(const QModelIndex &index);
+       void on_myListView_removeFileLocationRequested(int id);
+
        void on_filterInput_textChanged(const QString &filter);
        void on_filterType_currentIndexChanged(int);
 
@@ -44,23 +49,16 @@ private slots:
        void on_filterInput_keyDownPressed();
        void on_filterInput_returnPressed();
 
-       void currentSelectionChanged(const QModelIndex &current, const QModelIndex &previous);
-       void currentSelectionChanged();
-
        void on_modelQuery_returnPressed();
 
        void on_modelQueryButton_clicked();
 
 private:
-       void updateSelection();
-
        Ui::DynamicModelTab *ui;
 
-       MyListFilterModel *myListFilterModel;
+       DynamicModelFilterModel *dynamicModelFilterModel;
        LocalMyList::DynamicModel::DataModel *dataModel;
        LocalMyList::DynamicModel::Model *model;
-
-       int selectedRow;
 };
 
 #endif // DYNAMICMODELTAB_H
index 8cf1db4cf6caf3e8c3706a863cb4bf224d5057e2..c06b755039b2905fb6a96bccb992f40f86bf99f6 100644 (file)
@@ -20,6 +20,9 @@
    <property name="rightMargin">
     <number>0</number>
    </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
    <item>
     <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="0">
@@ -45,7 +48,7 @@
     </layout>
    </item>
    <item>
-    <widget class="MyListView" name="myListView"/>
+    <widget class="DynamicModelView" name="myListView"/>
    </item>
   </layout>
  </widget>
@@ -56,9 +59,9 @@
    <header>filterlineedit.h</header>
   </customwidget>
   <customwidget>
-   <class>MyListView</class>
+   <class>DynamicModelView</class>
    <extends>QTreeView</extends>
-   <header>mylistview.h</header>
+   <header>dynamicmodelview.h</header>
   </customwidget>
  </customwidgets>
  <resources/>
index 03f99dd5491895668f82ffb3fc3d7fd4dc574d1e..8633dea3a302693cef33b9ed8c6b2e528dfe6b97 100644 (file)
@@ -2095,6 +2095,9 @@ void Database::subscribeToNotifications()
        d->db.driver()->subscribeToNotification("episode_insert");
        d->db.driver()->subscribeToNotification("file_insert");
        d->db.driver()->subscribeToNotification("file_location_insert");
+       d->db.driver()->subscribeToNotification("anime_delete");
+       d->db.driver()->subscribeToNotification("episode_delete");
+       d->db.driver()->subscribeToNotification("file_delete");
        d->db.driver()->subscribeToNotification("file_location_delete");
 }
 
@@ -2399,6 +2402,42 @@ void Database::handleNotification(const QString &name)
                if (locationId)
                        emit fileLocationInsert(locationId, fid);
        }
+       else if (name == "anime_delete")
+       {
+               int id = payload.toInt();
+               if (id)
+                       emit animeDelete(id);
+       }
+       else if (name == "episode_delete")
+       {
+               QStringList ids = payload.toString().split(QChar(','), QString::SkipEmptyParts);
+               int eid = 0;
+               int aid = 0;
+               if (ids.count())
+                       eid = ids.takeFirst().toInt();
+               if (ids.count())
+                       aid = ids.takeFirst().toInt();
+
+               if (eid)
+                       emit episodeDelete(eid, aid);
+       }
+       else if (name == "file_delete")
+       {
+               QStringList ids = payload.toString().split(QChar(','), QString::SkipEmptyParts);
+               int fid = 0;
+               int eid = 0;
+               int aid = 0;
+
+               if (ids.count())
+                       fid = ids.takeFirst().toInt();
+               if (ids.count())
+                       eid = ids.takeFirst().toInt();
+               if (ids.count())
+                       aid = ids.takeFirst().toInt();
+
+               if (fid)
+                       emit fileDelete(fid, eid, aid);
+       }
        else if (name == "file_location_delete")
        {
                QStringList ids = payload.toString().split(QChar(','), QString::SkipEmptyParts);
index de7e97a24e9f5581581f3837e77c7bb52c0a98aa..9ee38a9ecb86f884e5f5cd719f7c1cae61fc5996 100644 (file)
@@ -81,7 +81,7 @@ public slots:
 
        /**
         * @brief previousEpisode return the prefioud available episode
-        * @param fid the id for which the previous episode is to be found
+        * @param fid the file id for which the previous episode is to be found
         * @return OpenFileData with fid != 0 if a previous episode is found
         */
        LocalMyList::OpenFileData previousEpisode(int fid);
@@ -219,6 +219,9 @@ signals:
        void fileInsert(int fid, int eid, int aid);
        void fileLocationInsert(int locationId, int fid);
 
+       void animeDelete(int aid);
+       void episodeDelete(int eid, int aid);
+       void fileDelete(int fid, int eid, int aid);
        void fileLocationDelete(int locationId, int fid);
 
 private slots:
index 3fa9e33804d3c1203b9111bfca2f47e06071a4cc..e613c99528781ae1e21b883475b9e5dc8074571a 100644 (file)
@@ -1,7 +1,8 @@
-#include "data.h"
+#include "dynamicmodel/data.h"
 
-#include "node.h"
-#include "datatype.h"
+#include "dynamicmodel/node.h"
+#include "dynamicmodel/datatype.h"
+#include "mylist.h"
 
 #include <QDebug>
 
@@ -35,13 +36,43 @@ Data::~Data()
        Q_ASSERT(references.isEmpty());
 }
 
-QVariant Data::data(int row, int role) const
+QVariant Data::primaryValue() const
 {
-       Q_UNUSED(row);
+       return id();
+}
+
+QVariant Data::data(int column, int role) const
+{
+       Q_UNUSED(column);
        Q_UNUSED(role);
        return QVariant();
 }
 
+bool Data::setData(int column, const QVariant &data, int role)
+{
+       Q_UNUSED(column);
+       Q_UNUSED(data);
+       Q_UNUSED(role);
+       return false;
+}
+
+Qt::ItemFlags Data::flags(int column) const
+{
+       Q_UNUSED(column);
+       return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+bool Data::matchesFilter(const QRegExp &filter) const
+{
+       return data(0, Qt::DisplayRole).toString().contains(filter);
+}
+
+bool Data::isVoteColumn(int column) const
+{
+       Q_UNUSED(column);
+       return false;
+}
+
 void Data::ref(Node *node)
 {
        Q_ASSERT(!references.contains(node));
@@ -51,11 +82,11 @@ void Data::ref(Node *node)
 
 void Data::deref(Node *node)
 {
-       Q_ASSERT(references.isEmpty());
+       Q_ASSERT(!references.isEmpty());
 
        bool removed = references.removeOne(node);
 
-       Q_ASSERT_X(removed, "deref", "Removing node that was not referenced");
+       Q_ASSERT_X(removed, "dynamicmodel/deref", "Removing node that was not referenced");
        Q_UNUSED(removed);
 
        if (references.isEmpty())
@@ -64,22 +95,52 @@ void Data::deref(Node *node)
 
 void Data::updated(Data *oldData)
 {
-       Q_UNUSED(oldData);
-       foreach (Node *node, references)
+       for (Node *node : references)
        {
-               Q_ASSERT_X(node->parent(), "dynamicmodel", "Updating node without parent");
-               node->updated(UpdateOperation);
-//             node->parent()->childUpdate(node, oldData, UpdateOperation);
+               Q_ASSERT_X(node->parent(), "dynamicmodel/updated", "Updating node without parent");
+               node->updated(oldData);
        }
 }
 
-void Data::added(Data *newData)
+void Data::deleted()
 {
-       foreach (Node *node, references)
+       // A copy is needed as nodes will deref this data
+       // If all refs get deleted this instance will also get deleted
+       auto refCopy = references;
+       for (Node *node : refCopy)
        {
-               if (node->childDataType() == newData->type())
-                       node->childAdded(newData);
+               Q_ASSERT_X(node->parent(), "dynamicmodel", "Deleting node without parent");
+               node->parent()->childDeleted(node);
        }
+       // this should be invalid here
+}
+
+ColumnData::ColumnData(DataType *dataType) : Data{dataType}
+{
+
+}
+
+ColumnData &ColumnData::operator=(ColumnData &other)
+{
+       value = other.value;
+       return *this;
+}
+
+int ColumnData::id() const
+{
+       return 0;
+}
+
+QVariant ColumnData::primaryValue() const
+{
+       return value;
+}
+
+QVariant ColumnData::data(int column, int role) const
+{
+       if (column != 0) return {};
+       if (role != Qt::DisplayRole) return {};
+       return value;
 }
 
 AnimeData::AnimeData(DataType *dataType) : Data(dataType)
@@ -101,8 +162,20 @@ int AnimeData::id() const
        return animeData.aid;
 }
 
+Qt::ItemFlags AnimeData::flags(int column) const
+{
+       Qt::ItemFlags flags = Data::flags(column);
+       if (column == 3)
+               flags |= Qt::ItemIsEditable;
+       return flags;
+}
+
+
 QVariant AnimeData::data(int column, int role) const
 {
+       static const QString epCountString{"%1%3 of %2%4"};
+       static const QString unknownEpCountString{"%1%3 of (%2%4)"};
+       static const QString specialsCountString{"+%1"};
        switch (role)
        {
                case Qt::DisplayRole:
@@ -111,13 +184,13 @@ QVariant AnimeData::data(int column, int role) const
                                case 0:
                                        return animeData.titleRomaji;
                                case 1:
-                                       if (animeData.totalEpisodeCount)
-                                               return QString("%1 of %2")
-                                                               .arg(episodesInMyList).arg(animeData.totalEpisodeCount);
-                                       return QString("%1 of (%2)")
+                                       return (animeData.totalEpisodeCount ? epCountString : unknownEpCountString)
                                                        .arg(episodesInMyList)
-                                                       .arg(qMax(animeData.highestEpno,
-                                                                       episodesInMyList));
+                                                       .arg(animeData.totalEpisodeCount
+                                                                ? animeData.totalEpisodeCount
+                                                                : qMax(animeData.highestEpno, episodesInMyList))
+                                                       .arg(specialsInMyList ? specialsCountString.arg(specialsInMyList) : "")
+                                                       .arg("");
                                case 2:
                                        if (animeData.rating < 1)
                                                return "n/a";
@@ -127,8 +200,10 @@ QVariant AnimeData::data(int column, int role) const
                                                return "n/a";
                                        return QString::number(animeData.myVote, 'f', 2);
                                case 4:
-                                       return QString("%1 of %2").arg(watchedEpisodes)
-                                                       .arg(episodesInMyList);
+                                       return epCountString.arg(watchedEpisodes)
+                                                       .arg(episodesInMyList)
+                                                       .arg(specialsInMyList ? specialsCountString.arg(watchedSpecials) : "")
+                                                       .arg(specialsInMyList ? specialsCountString.arg(specialsInMyList) : "");
                                case 5:
                                        return stateIdToState(myState);
                        }
@@ -158,6 +233,51 @@ QVariant AnimeData::data(int column, int role) const
        return QVariant();
 }
 
+bool AnimeData::setData(int column, const QVariant &data, int role)
+{
+       if (role != Qt::EditRole)
+               return false;
+
+       switch (column)
+       {
+               case 3:
+               {
+                       double vote = data.toDouble();
+
+                       if (qFuzzyCompare(animeData.myVote, vote))
+                               return false;
+
+                       if (vote < 1.0 || vote > 10.0)
+                               vote = 0;
+
+                       animeData.myVote = vote;
+
+                       MyList::instance()->voteAnime(animeData.aid, vote);
+
+                       return true;
+               }
+       }
+       return false;
+}
+
+bool AnimeData::matchesFilter(const QRegExp &filter) const
+{
+       if (Data::matchesFilter(filter))
+               return true;
+
+       for (auto &&title : alternateTitles)
+       {
+               if (title.contains(filter))
+                       return true;
+       }
+       return false;
+}
+
+bool AnimeData::isVoteColumn(int column) const
+{
+       return column == 3;
+}
+
 // ==========================================================
 
 EpisodeData::EpisodeData(DataType *dataType) : Data(dataType)
@@ -230,6 +350,11 @@ QVariant EpisodeData::data(int column, int role) const
        return QVariant();
 }
 
+bool EpisodeData::isVoteColumn(int column) const
+{
+       return column == 3;
+}
+
 FileData::FileData(DataType *dataType) : Data(dataType)
 {
 }
@@ -308,7 +433,7 @@ QVariant FileLocationData::data(int column, int role) const
                        if (!fileLocationData.renamed.isValid())
                                return QObject::tr("No");
                        if (fileLocationData.failedRename)
-                               return QObject::tr("Rename failed");
+                               return QObject::tr("Rename failed: %1").arg(fileLocationData.renameError);
                        return QObject::tr("Yes, on %1").arg(fileLocationData.renamed.toString());
        }
        return QVariant();
index aa2a92ecdcd9dfd95c7d551fbaa48000b4528dd7..a765b3f786afe005d6da9b52b513d066dedf5873 100644 (file)
@@ -22,7 +22,13 @@ public:
        virtual ~Data();
 
        virtual int id() const = 0;
-       virtual QVariant data(int row, int role) const;
+       virtual QVariant primaryValue() const;
+       virtual Qt::ItemFlags flags(int column) const;
+       virtual QVariant data(int column, int role) const;
+       virtual bool setData(int column, const QVariant &data, int role);
+       virtual bool matchesFilter(const QRegExp &filter) const;
+
+       virtual bool isVoteColumn(int column) const;
 
        DataType *type() const { return m_type; }
        // Referencing
@@ -30,26 +36,47 @@ public:
        void deref(Node *node);
 
        void updated(Data *oldData);
-       void added(Data *newData);
+       void deleted();
 
 private:
        NodeList references;
        DataType * const m_type;
 };
 
+class LOCALMYLISTSHARED_EXPORT ColumnData : public Data
+{
+public:
+       ColumnData(DataType *dataType);
+       ColumnData &operator=(ColumnData &other);
+
+       int id() const override;
+       QVariant primaryValue() const override;
+       QVariant data(int column, int role) const override;
+
+       QVariant value;
+};
+
 class LOCALMYLISTSHARED_EXPORT AnimeData : public Data
 {
 public:
        AnimeData(DataType *dataType);
        AnimeData &operator=(AnimeData &other);
 
-       int id() const;
-       QVariant data(int column, int role) const;
+       int id() const override;
+       virtual Qt::ItemFlags flags(int column) const override;
+       QVariant data(int column, int role) const override;
+       bool setData(int column, const QVariant &data, int role) override;
+       bool matchesFilter(const QRegExp &filter) const override;
+
+       bool isVoteColumn(int column) const override;
 
        Anime animeData;
        int episodesInMyList;
+       int specialsInMyList;
        int watchedEpisodes;
+       int watchedSpecials;
        int myState;
+       QList<QString> alternateTitles;
 };
 
 class LOCALMYLISTSHARED_EXPORT EpisodeData : public Data
@@ -58,8 +85,10 @@ public:
        EpisodeData(DataType *dataType);
        EpisodeData &operator=(EpisodeData &other);
 
-       int id() const;
-       QVariant data(int column, int role) const;
+       int id() const override;
+       QVariant data(int column, int role) const override;
+
+       bool isVoteColumn(int column) const override;
 
        Episode episodeData;
        QDateTime watchedDate;
@@ -73,8 +102,8 @@ public:
        FileData(DataType *dataType);
        FileData &operator=(FileData &other);
 
-       int id() const;
-       QVariant data(int column, int role) const;
+       int id() const override;
+       QVariant data(int column, int role) const override;
 
        File fileData;
 };
@@ -85,8 +114,8 @@ public:
        FileLocationData(DataType *dataType);
        FileLocationData &operator=(FileLocationData &other);
 
-       int id() const;
-       QVariant data(int column, int role) const;
+       int id() const override;
+       QVariant data(int column, int role) const override;
 
        FileLocation fileLocationData;
        QString hostName;
@@ -98,8 +127,8 @@ public:
        AnimeTitleData(DataType *dataType);
        AnimeTitleData &operator=(AnimeTitleData &other);
 
-       int id() const;
-       QVariant data(int column, int role) const;
+       int id() const override;
+       QVariant data(int column, int role) const override;
 
        AnimeTitle animeTitleData;
 };
index 7e7232df3e032db13f751e7dc04d7eda8213d3e4..f3ad417832628a40caf4fe85032e25007dd6a8e8 100644 (file)
@@ -1,7 +1,9 @@
-#include "datamodel.h"
+#include "dynamicmodel/datamodel.h"
 
-#include "datatype.h"
-#include "typerelation.h"
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/typerelation.h"
+
+#include <QDebug>
 
 namespace LocalMyList {
 namespace DynamicModel {
@@ -12,7 +14,10 @@ DataModel::DataModel(QObject *parent) : QObject(parent)
 
 DataModel::~DataModel()
 {
+       for (auto &&relations : typeRelations)
+               qDeleteAll(relations);
        qDeleteAll(dataTypes);
+       qDebug() << "Deleted data model";
 }
 
 bool DataModel::registerDataType(DataType *dataType)
@@ -58,24 +63,44 @@ bool DataModel::registerTypeRelation(TypeRelation *typeRelation)
 DataType *DataModel::dataType(const QString &name) const
 {
        DataType *t = dataTypeNames.value(name, 0);
-       Q_ASSERT(t);
+       Q_ASSERT_X(t, "dynamicmodel", "Unregistered data type requested.");
        return t;
 }
 
+bool DataModel::hasDataType(const QString &name) const
+{
+       return dataTypeNames.value(name, 0);
+}
+
 TypeRelation *DataModel::typeRelation(const QString &source, const QString &destiantion)
 {
        const auto it = typeRelations.find(source);
 
        if (it == typeRelations.constEnd())
+       {
+               Q_ASSERT_X(false, "dynamicmodel", "Unregistered typerelation Requested (source)");
                return 0;
+       }
 
        const auto inner = it.value().find(destiantion);
 
        if (inner == it.value().constEnd())
+       {
+               Q_ASSERT_X(false, "dynamicmodel", "Unregistered typerelation Requested (destination)");
                return 0;
+       }
 
        return inner.value();
 }
 
+bool DataModel::hasTypeRelation(const QString &source, const QString &destiantion) const
+{
+       const auto it = typeRelations.find(source);
+       if (it == typeRelations.constEnd())
+               return false;
+       const auto inner = it.value().find(destiantion);
+       return inner != it.value().constEnd();
+}
+
 } // namespace DynamicModel
 } // namespace LocalMyList
index 8f589ac4cabdca646292c9e6ee35e17ca2212992..6d907d859dc56032ce107cde793f5b4ed97b126e 100644 (file)
@@ -28,9 +28,13 @@ public:
        bool registerTypeRelation(TypeRelation *typeRelation);
 
        DataType *dataType(const QString &name) const;
+       bool hasDataType(const QString &name) const;
        TypeRelation *typeRelation(const QString &source, const QString &destiantion);
+       bool hasTypeRelation(const QString &source, const QString &destiantion) const;
 
 
+signals:
+       void entryAdded(DataType *dataType, int id);
 
 private slots:
 /*     void animeUpdate(int aid);
index 3d33f03627d6cd82a9abc7b46d8fb5a118cb45c5..063b0929e8e4af439c8c39e032d9118fa6656100 100644 (file)
@@ -2,8 +2,10 @@
 
 #include <QtGlobal>
 #include <QSqlQuery>
-#include "../database.h"
-#include "../mylist.h"
+#include "database.h"
+#include "mylist.h"
+
+#include <QDebug>
 
 namespace LocalMyList {
 namespace DynamicModel {
@@ -23,9 +25,19 @@ DataModel *DataType::model() const
        return m_model;
 }
 
-QStringList DataType::availableChildRelations() const
+QString DataType::name() const
+{
+       return tableName();
+}
+
+QString DataType::orderBy() const
+{
+       return {};
+}
+
+QString DataType::additionalJoins() const
 {
-       return QStringList();
+       return {};
 }
 
 Data *DataType::data(int key) const
@@ -41,30 +53,94 @@ void DataType::unregistered()
 {
 }
 
+NodeList DataType::readEntries(SqlResultIteratorInterface &it, Node *parent)
+{
+       qDebug() << "readEntries" << tableName();
+       NodeList ret;
+       while (it.next())
+       {
+               int totalRowCount = it.value(0).toInt();
+               Data *data = readEntry(it);
+               if (data->id())
+               {
+                       auto it = m_dataStore.find(data->id());
+                       if (it != m_dataStore.end())
+                               data = it.value();
+                       else
+                               m_dataStore.insert(data->id(), data);
+               }
+               Node *node = new Node(parent->model(), parent, totalRowCount, data);
+               ret << node;
+       }
+       return ret;
+}
+
+QString DataType::updateQuery() const
+{
+       return QString{R"(
+               SELECT 0, %1
+                               FROM %2 %3
+                               %5
+                       WHERE %3.%4 = :id
+       )"}
+               .arg(additionalColumns())
+               .arg(tableName())
+               .arg(alias())
+               .arg(primaryKeyName())
+               .arg(additionalJoins());
+}
+
 void DataType::update(Data *data)
 {
        Q_UNUSED(data);
 }
 
-void DataType::childUpdate(Node *parent, const Data *oldData, Operation operation)
+void DataType::childUpdated(Node *child, const Data * const oldData)
 {
-       Q_UNUSED(parent);
+       Q_UNUSED(child);
        Q_UNUSED(oldData);
-       Q_UNUSED(operation);
+}
+
+void DataType::deleted(Data *data)
+{
+       int id = data->id();
+       data->deleted();
+       if (id)
+               m_dataStore.remove(id);
 }
 
 void DataType::released(Data *data)
 {
-       Q_ASSERT(data != 0);
+       Q_ASSERT_X(data, "dynamicmodel/released", "released() got NULL data");
 
-       bool removed = m_dataStore.remove(data->id());
+       if (data->id())
+       {
+               bool removed = m_dataStore.remove(data->id());
 
-       Q_ASSERT_X(removed, "released", "releasing node not in data store");
-       Q_UNUSED(removed);
+               Q_ASSERT_X(removed, "dynamicmodel/released", "releasing node not in data store");
+               Q_UNUSED(removed);
+       }
 
        delete data;
 }
 
+NodeCompare DataType::nodeCompareFunction() const
+{
+       return [](Node *a, Node *b)
+       {
+               return a->data()->primaryValue() < b->data()->primaryValue();
+       };
+}
+
+QList<QString> DataType::availableActions() const
+{
+       return {};
+}
+
+void DataType::actionRequested(int)
+{
+}
+
 int DataType::sizeHelper(const QString &tableName, const QString &keyName) const
 {
        if (m_size)
index 3792df1f1738708a53d93c601f6a9a34fadbd481..8d2b07ae6a35ee96f0f8bb81ea487f27f6f1620f 100644 (file)
@@ -29,30 +29,77 @@ public:
 
        DataModel *model() const;
 
-       virtual QString name() const = 0;
-       QStringList availableChildRelations() const;
-
-       virtual QString baseQuery() const = 0;
+       /**
+        * @brief The name of the data type
+        * @return table name
+        */
+       virtual QString name() const;
+
+       /**
+        * @brief The name of the table this data type represents
+        * @return table name
+        */
+       virtual QString tableName() const = 0;
+
+       /**
+        * @brief The alias alias to the table returned by name()
+        * @return table alias
+        */
+       virtual QString alias() const = 0;
+
+       /**
+        * @brief The name of the primary key column in the table returned by name()
+        * @return name of the primary key
+        */
+       virtual QString primaryKeyName() const = 0;
+
+       /**
+        * @brief comma separated list of columns prefixed with the table alias that
+        * this data type requires.
+        * @return columns
+        */
+       virtual QString additionalColumns() const = 0;
+
+       /**
+        * @brief SQL ORDER BY clause
+        * @return SQL ORDER BY clause or empty string if ordering is not required
+        */
+       virtual QString orderBy() const;
+
+       /**
+        * @brief Additional joins for columns this data type requires.
+        * @return join statements or empty string
+        */
+       virtual QString additionalJoins() const;
 
        Data *data(int key) const;
-       virtual int size() const = 0;
 
        // Register
        virtual void registerd();
        virtual void unregistered();
 
+       // Obtain
+       virtual NodeList readEntries(SqlResultIteratorInterface &it, Node *parent);
+
        // Update
+       virtual QString updateQuery() const;
+
        virtual void update(Data *data);
-       virtual void childUpdate(Node *child, const Data *oldData, Operation operation);
+       virtual void childUpdated(Node *child, const Data *const oldData);
+       virtual void deleted(Data *data);
 
        // Release
        void released(Data *data);
 
-       virtual NodeCompare nodeCompareFunction() const = 0;
+       virtual NodeCompare nodeCompareFunction() const;
 
        // Type relation interface
        virtual Data *readEntry(const SqlResultIteratorInterface &it) = 0;
 
+       // Actions
+       virtual QList<QString> availableActions() const;
+       virtual void actionRequested(int action);
+
 protected:
        int sizeHelper(const QString &tableName, const QString &keyName) const;
 
@@ -63,16 +110,6 @@ protected:
                func(*typedData, it);
 
                Data *newData = typedData;
-               Data *currentData = data(typedData->id());
-               if (currentData)
-               {
-                       delete typedData;
-                       newData = currentData;
-               }
-               else
-               {
-                       m_dataStore.insert(typedData->id(), newData);
-               }
                return newData;
        }
 
index d55180fb0665b8ab3e2afdffab8ecc637d3d27df..636bd5e6ed39032f81aa8f3bd13306e740180211 100644 (file)
@@ -1,39 +1,56 @@
-#include "model.h"
+#include "dynamicmodel/model.h"
 
-#include "node.h"
-#include "datamodel.h"
-#include "datatype.h"
-#include "typerelation.h"
+#include "dynamicmodel/node.h"
+#include "dynamicmodel/datamodel.h"
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/typerelation.h"
+#include "dynamicmodel/queryparser.h"
+#include "mylist.h"
+#include <QDebug>
 
 namespace LocalMyList {
 namespace DynamicModel {
 
 Model::Model(QObject *parent) :
-       QAbstractItemModel(parent), m_dataModel(0)
+       QAbstractItemModel(parent)
 {
        rootItem = createRootNode();
 }
 
 Model::~Model()
 {
+       qDebug() << "deleting model";
        delete rootItem;
+       qDebug() << "deleted model";
 }
 
-QString Model::query() const
+QueryParser Model::query() const
 {
        return m_query;
 }
 
-void Model::setQuery(const QString &query)
+void Model::setQuery(const QueryParser &query)
 {
        if (query == m_query)
                return;
 
-       dataTypeNames = query.split(QChar('|'));
-       reload();
+       if (!query.isValid())
+               return;
+
+       if (m_query.dataModel() != query.dataModel())
+       {
+               if (m_query.dataModel())
+                       disconnect(m_query.dataModel(), 0, this, 0);
+               if (query.dataModel())
+                       connect(query.dataModel(), SIGNAL(entryAdded(DataType*,int)), this, SLOT(entryAdded(DataType*,int)));
+       }
 
        m_query = query;
+
+       reload();
+
        emit queryChanged(query);
+       emit queryChanged(query.query());
 }
 
 QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
@@ -49,7 +66,11 @@ Qt::ItemFlags Model::flags(const QModelIndex &index) const
        if (!index.isValid())
                return 0;
 
-       return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+       Node *node = static_cast<Node *>(index.internalPointer());
+       if (!node->data())
+               return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+
+       return node->data()->flags(index.column());
 }
 
 QVariant Model::data(const QModelIndex &index, int role) const
@@ -62,6 +83,21 @@ QVariant Model::data(const QModelIndex &index, int role) const
        return item->data(index.column(), role);
 }
 
+bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+       if (!index.isValid())
+               return false;
+
+       Node *item = static_cast<Node *>(index.internalPointer());
+
+       bool ret = item->setData(index.column(), value, role);
+
+       if (ret)
+               emit dataChanged(index, index);
+
+       return ret;
+}
+
 QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
 {
        if (!hasIndex(row, column, parent))
@@ -162,28 +198,21 @@ QModelIndex Model::index(Node *node) const
        return createIndex(node->row(), 0, node);
 }
 
-DataModel *Model::dataModel() const
-{
-       return m_dataModel;
-}
-
 DataType *Model::rootDataType() const
 {
-       return m_dataModel->dataType("anime");
+       return m_query.dataModel()->dataType("anime");
 }
 
-DataType *Model::grandChildDataType(Node *node) const
+DataModel *Model::dataModel() const
 {
-       int d = node->depth() + 1;
-
-       return childDataType(d);
+       return m_query.dataModel();
 }
 
 DataType *Model::childDataType(int i) const
 {
-       if (dataTypeNames.count() <= i)
+       if (i > m_query.levels())
                return 0;
-       return dataModel()->dataType(dataTypeNames.at(i));
+       return m_query.dataType(i);
 }
 
 void Model::reload()
@@ -194,65 +223,134 @@ void Model::reload()
        endResetModel();
 }
 
-void Model::setDataModel(DataModel *dataModel)
+void Model::entryAdded(DataType *dataType, int id)
 {
-       if (m_dataModel == dataModel)
-               return;
+       qDebug() << "entryAdded" << dataType << id;
+       for (int i = 0; i < m_query.levels(); ++i)
+       {
+               if (dataType == m_query.dataType(i))
+               {
+                       newEntryCheck(i, id, dataType);
+               }
+       }
+}
+
+void Model::episodeInsert(int aid, int eid)
+{
+       Q_UNUSED(aid);
+       Q_UNUSED(eid);
+//     DataType *episodeDataType = m_query.dataModel()->dataType("episode");
 
-       m_dataModel = dataModel;
-       emit dataModelChanged(dataModel);
+//     if (!episodeDataType)
+//             return;
 
-       reload();
+//     if (!m_query.dataModel()->dataType("anime"))
+//             return;
+
+//     QString previousDataTypeName = QString();
+////   DataType *previousDataType = 0;
+
+//     for (const QString &dataTypeName : m_query.dataTypeNames())
+//     {
+//             DataType *currentDataType = m_query.dataModel()->dataType(dataTypeName);
+
+//             if (currentDataType == episodeDataType)
+//             {
+//                     TypeRelation *rel = m_query.dataModel()->typeRelation(previousDataTypeName, dataTypeName);
+
+//                     if (previousDataTypeName.isNull())
+//                     {
+//                             // The root is the parent, just see if it needs to be added.
+//                     }
+//                     else
+//                     {
+//                             IdList ids = rel->getParents(eid);
+
+
+//                     }
+//             }
+
+//             previousDataTypeName = dataTypeName;
+//     }
 }
 
-void Model::episodeInsert(int aid, int eid)
+Node *Model::createRootNode()
 {
-       DataType *episodeDataType = m_dataModel->dataType("episode");
+       int size = rootNodeSize();
+       Node *n = new Node(this, 0, size, 0);
+       qDebug() << "SIZE" << size;
+       return n;
+}
 
-       if (!episodeDataType)
-               return;
+int Model::rootNodeSize() const
+{
+       if (!m_query.isValid())
+               return 0;
 
-       if (!m_dataModel->dataType("anime"))
-               return;
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+                               query().buildCountSql(-1));
+
+       if (!MyList::instance()->database()->exec(q))
+               return 0;
 
-       QString previousDataTypeName = QString();
-       DataType *previousDataType = 0;
+       if (!q.next())
+               return 0;
+
+       int count = q.value(0).toInt();
+
+       q.finish();
+
+       return count;
+}
 
-       for (const QString &dataTypeName : dataTypeNames)
+void Model::newEntryCheck(int currentLevel, int id, DataType *dataType)
+{
+       qDebug() << "newEntryCheck" << currentLevel << id << dataType;
+       // Children of the rootNode don't need any checks
+       if (!currentLevel)
        {
-               DataType *currentDataType = m_dataModel->dataType(dataTypeName);
+               rootItem->childAdded(id, dataType);
+               return;
+       }
 
-               if (currentDataType == episodeDataType)
-               {
-                       TypeRelation *rel = m_dataModel->typeRelation(previousDataTypeName, dataTypeName);
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+                               query().buildPrimaryValuesSql(currentLevel));
 
-                       if (previousDataTypeName.isNull())
-                       {
-                               // The root is the parent, just see if it needs to be added.
-                       }
-                       else
-                       {
-                               IdList ids = rel->getParents(eid);
+       q.bindValue(":id", id);
 
+       if (!MyList::instance()->database()->exec(q))
+               return;
 
-                       }
+       QSqlResultIterator it(q);
+       while (it.next())
+       {
+               QVariantList primaryValues;
+               for (int i = 0; i < currentLevel; ++i)
+               {
+                       primaryValues << it.value(i);
                }
 
-               previousDataTypeName = dataTypeName;
+               Node *parent = rootItem->findParentOfNewEntry(primaryValues);
+
+               if (!parent)
+                       continue;
+
+               // TODO this will fetch the data from the DB for every parent (it's always the same data)
+               // entryAddedToNode to be implemented for handling this
+               parent->childAdded(id, dataType);
        }
+       q.finish();
 }
 
-Node *Model::createRootNode()
+Data *Model::entryAddedToNode(Node *node, int id, DataType *dataType, Data *data)
 {
-       int size = (m_dataModel && !dataTypeNames.isEmpty())
-                               ? dataModel()->dataType(dataTypeNames.at(0))->size()
-                               : 0;
-       Node *n = new Node(this, 0, size, 0);
-       if (m_dataModel && !dataTypeNames.isEmpty())
-               n->setChildDataType(dataModel()->dataType(dataTypeNames.at(0)));
-       return n;
+       Q_UNUSED(node);
+       Q_UNUSED(id);
+       Q_UNUSED(dataType);
+       Q_UNUSED(data);
+       return 0;
 }
 
 
 } // namespace DynamicModel
-} // namespace Local
+} // namespace LocalMyList
index 037a329c86afa51817c2aa42ea34789da58541e8..798843eb86179e0fe4f2f4b6957c24de40cc2f83 100644 (file)
@@ -2,6 +2,7 @@
 #define MODEL_H
 
 #include "../localmylist_global.h"
+#include "dynamicmodel/queryparser.h"
 #include <QAbstractItemModel>
 #include <QStringList>
 
@@ -11,65 +12,69 @@ namespace DynamicModel {
 class Node;
 class DataModel;
 class DataType;
+class Query;
 
 class LOCALMYLISTSHARED_EXPORT Model : public QAbstractItemModel
 {
        Q_OBJECT
-       Q_PROPERTY(DataModel* dataModel READ dataModel WRITE setDataModel NOTIFY dataModelChanged)
-       Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged)
+       Q_PROPERTY(QueryParser query READ query WRITE setQuery NOTIFY queryChanged)
        friend class Node;
 
 public:
        explicit Model(QObject *parent = 0);
        ~Model();
 
-       QString query() const;
-       void setQuery(const QString &query);
+       QueryParser query() const;
+       void setQuery(const QueryParser &query);
 
-       QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
-       Qt::ItemFlags flags(const QModelIndex &index) const;
-       QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
-       QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
-       QModelIndex parent(const QModelIndex &index) const;
+       // Data
+       QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+       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 = Qt::EditRole) override;
 
-       int rowCount(const QModelIndex &parent = QModelIndex()) const;
-       int columnCount(const QModelIndex &parent = QModelIndex()) const;
+       // Structure
+       QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
+       QModelIndex parent(const QModelIndex &index) const override;
+
+       // Dimensions
+       int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+       int columnCount(const QModelIndex &parent = QModelIndex()) const override;
 
        // Lazy loading
-       bool canFetchMore(const QModelIndex &parent) const;
-       void fetchMore(const QModelIndex &parent);
-       bool hasChildren(const QModelIndex &parent = QModelIndex()) const;
+       bool canFetchMore(const QModelIndex &parent) const override;
+       void fetchMore(const QModelIndex &parent) override;
+       bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
 
        Node *node(const QModelIndex &idx) const;
        QModelIndex index(Node *node) const;
 
-       DataModel *dataModel() const;
-
        DataType *rootDataType() const;
+       DataModel *dataModel() const;
 
-       DataType *grandChildDataType(Node *node) const;
        DataType *childDataType(int i) const;
 
 public slots:
        void reload();
 
-       void setDataModel(DataModel *dataModel);
-
 private slots:
+       void entryAdded(DataType *dataType, int id);
        void episodeInsert(int aid, int eid);
 
 signals:
-       void dataModelChanged(DataModel *dataModel);
+       void queryChanged(QueryParser query);
        void queryChanged(QString query);
 
 private:
        Node *createRootNode();
+       int rootNodeSize() const;
+
+       void newEntryCheck(int currentLevel, int id, DataType *dataType);
+       Data *entryAddedToNode(Node *node, int id, DataType *dataType, Data *data);
 
        Node *rootItem;
-       DataModel* m_dataModel;
 
-       QStringList dataTypeNames;
-       QString m_query;
+       QueryParser m_query;
 };
 
 } // namespace DynamicModel
index 9baa6233c70df9b97afbf8bf299f12ca96910922..9160899b27622cccb8cec3914c2b0ec1ab945bcf 100644 (file)
@@ -1,10 +1,12 @@
-#include "node.h"
-#include "datatype.h"
-
-#include "dynamicmodel_global.h"
-#include "data.h"
-#include "model.h"
-#include "typerelation.h"
+#include "dynamicmodel/node.h"
+
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/dynamicmodel_global.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/model.h"
+#include "dynamicmodel/typerelation.h"
+#include "dynamicmodel/queryparser.h"
+#include "mylist.h"
 #include <QModelIndex>
 
 #include <QDebug>
@@ -14,7 +16,7 @@ namespace DynamicModel {
 
 Node::Node(Model *model, Node *parent, int totalRowCount, Data *data)
        : m_parent(parent), m_model(model), m_totalRowCount(totalRowCount),
-         m_data(data), m_childType(0)
+         m_data(data)
 {
        Q_ASSERT_X((parent && data) || (!parent && !data), "dynamic model", "Root node has no data and no parent. Other nodes must have both");
 
@@ -25,22 +27,15 @@ Node::Node(Model *model, Node *parent, int totalRowCount, Data *data)
 
 Node::~Node()
 {
-       if (!m_data)
-               return;
+       if (m_data)
+               m_data->deref(this);
 
-       m_data->deref(this);
        qDeleteAll(m_children);
 }
 
 DataType *Node::childDataType() const
 {
-       return m_childType;
-}
-
-void Node::setChildDataType(DataType *dataType)
-{
-//     Q_ASSERT_X(dataType, "dynamicmodel", "NULL data type");
-       m_childType = dataType;
+       return 0;
 }
 
 Node *Node::parent() const
@@ -60,7 +55,7 @@ int Node::childCount() const
 
 int Node::columnCount() const
 {
-       return 5;
+       return 6;
 }
 
 int Node::row() const
@@ -73,11 +68,16 @@ int Node::row() const
 
 bool Node::hasChildren() const
 {
-       if (this == m_model->rootItem)
-               return true;
+//     if (isRoot())
+//             return true;
        return totalRowCount() > 0;
 }
 
+Model *Node::model() const
+{
+       return m_model;
+}
+
 QVariant Node::data(int column, int role) const
 {
 //     qDebug() << parent() << column;
@@ -99,6 +99,8 @@ QVariant Node::data(int column, int role) const
                        return QObject::tr("Vote");
                case 4:
                        return QObject::tr("Watched / Renamed");
+               case 5:
+                       return QObject::tr("State");
        }
 
        return QVariant();
@@ -109,6 +111,13 @@ Data *Node::data() const
        return m_data;
 }
 
+bool Node::setData(int column, const QVariant &data, int role)
+{
+       if (!m_data)
+               return false;
+       return m_data->setData(column, data, role);
+}
+
 int Node::totalRowCount() const
 {
        return m_totalRowCount;// ? m_totalRowCount : childDataType() ? childDataType()->size() : 0;
@@ -126,31 +135,30 @@ bool Node::canFetchMore() const
 
 void Node::fetchMore()
 {
-       if (!m_childType)
-               return;
        qDebug() << "fetchMore" << this;
-       NodeList newItems;
-
-       TypeRelation *rel = 0;
-       if (isRoot())
-               rel = m_model->dataModel()->typeRelation(QString(), childDataType()->name());
-       else
-               rel = m_model->dataModel()->typeRelation(m_data->type()->name(), childDataType()->name());
 
-       if (!rel)
-               return;
+       DataType *dataType = model()->childDataType(level());
 
-       DataType *grandChildDataType = m_model->grandChildDataType(this);
 /*     qDebug() << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";
        qDebug() << "currentType\t" << (m_data ? m_data->type()->name() : "<root>");
        qDebug() << "grandChildDataType\t" << (grandChildDataType ? grandChildDataType->name() : QString("0"));
 //     qDebug() << "rowCountType\t" << (rowCountType ? rowCountType->name() : QString("0"));
        qDebug() << "getting from rel" << rel->sourceType() << rel->destinationType();
 */
-       auto factory = childNodeFactory();
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+                               m_model->query().buildSql(level()));
 
+       bindValues(q);
+       qDebug() << LIMIT << childCount();
+       q.bindValue(":limit", LIMIT);
+       q.bindValue(":offset", childCount());
 
-       newItems = rel->getChildren(m_data, childCount(), grandChildDataType, factory);
+       if (!MyList::instance()->database()->exec(q))
+               return;
+
+       QSqlResultIterator it(q);
+       NodeList newItems = dataType->readEntries(it, this);
+       q.finish();
 
        const QModelIndex parent = m_model->index(this);
        const int newrows = newItems.count();
@@ -172,6 +180,28 @@ void Node::fetchComplete()
 
 }
 
+Node *Node::findParentOfNewEntry(const QVariantList &primaryValues)
+{
+       // Not loaded
+       if (!m_children.size() && m_totalRowCount)
+               return nullptr;
+qDebug() << level() << "magic level" << primaryValues.size();
+       if (level() == primaryValues.size())
+               return this;
+
+       const QVariant &primaryValue = primaryValues.at(level());
+
+       Node *child = findChildByPrimaryValue(primaryValue);
+
+       if (!child)
+               return nullptr;
+
+       Q_ASSERT_X(child->parent() == this, "dynamicmodel/update", "Found node that is not child of it's parent");
+qDebug() << child->level() << "magic level";
+
+       return child->findParentOfNewEntry(primaryValues);
+}
+
 MoveType Node::moveChild(Node *child, Operation type)
 {
 qDebug() << "a";
@@ -255,10 +285,24 @@ qDebug() << "f";
        }
 qDebug() << "g";
 
-       return SuccessfulMove;
+return SuccessfulMove;
 }
 
-int Node::depth() const
+void Node::childDeleted(Node *child)
+{
+       Q_ASSERT(child);
+       Q_ASSERT_X(child->parent() == this, "dynamicmodel/node", "Deleting child of a different parent");
+
+       const QModelIndex idx = m_model->index(this);
+       const int row = child->row();
+       m_model->beginRemoveRows(idx, row, row);
+       m_children.removeAt(row);
+       --m_totalRowCount;
+       m_model->endRemoveRows();
+       delete child;
+}
+
+int Node::level() const
 {
        Node *node = parent();
        int depth = 0;
@@ -270,49 +314,90 @@ int Node::depth() const
        return depth;
 }
 
+Node *Node::findChildByPrimaryValue(const QVariant &primaryValue)
+{
+       // TODO this can be made logarithmic
+       // children of type column are ordered ascending by their valie
+       // datatypes with ids can bee looked up in the DataType's dataStore,
+       // followed by node lookup.
+       for (Node *child : m_children)
+       {
+               if (child->data()->primaryValue() == primaryValue)
+                       return child;
+       }
+       return 0;
+}
+
 NodeFactory Node::childNodeFactory()
 {
        return [=](Data *d, int c) -> Node *
        {
                Node *n = new Node(m_model, this, c, d);
-               n->setChildDataType(m_model->grandChildDataType(this));
+//             n->setChildDataType(m_model->grandChildDataType(this));
                return n;
        };
 }
 
+void Node::bindValues(QSqlQuery &query) const
+{
+       if (parent())
+               parent()->bindValues(query);
+       m_model->query().bindValue(query, m_data, level() - 1);
+}
+
 int Node::id() const
 {
        return m_data->id();
 }
 
-void Node::childAdded(Data *newData)
+bool Node::childAdded(int id, DataType *dataType)
 {
-       qDebug() << "childAdded" << newData;
+       qDebug() << "childAdded" << id << dataType;
 
-/*     Node *childNode = childNodeFactory()(newData);
+       // The total row count increases regardless if we actually add a child or not
+       // as there is a row in the db that is a child of this parent
+       ++m_totalRowCount;
 
-       MoveType moveType = moveChild(childNode, InsertOperation);
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+                               m_model->query().buildEntrySql(level()));
+       bindValues(q);
+       q.bindValue(":id", id);
 
-       if (moveType == OutOfBoundsMove)
-               delete childNode;
-*/
-}
-/*
-void Node::childUpdate(Node *child, const Data *newData, Operation operation)
-{
+       if (!q.exec())
+               return false;
+
+       Node *child = nullptr;
+
+       {
+               QSqlResultIterator it(q);
+               NodeList nodelist = dataType->readEntries(it, this);
+               q.finish();
+               if (!nodelist.length())
+                       return false;
+               child = nodelist.at(0);
+       }
 
+       // TODO pg orders differently than the usual node compare leading to arbitrary ordering
+       auto it = std::upper_bound(m_children.begin(), m_children.end(), child, dataType->nodeCompareFunction());
+       if (it == m_children.end())
+               return false;
+       const int newRow = qMax(0, (it - m_children.begin()) - 1);
+       m_model->beginInsertRows(m_model->index(this), newRow, newRow);
+       it = m_children.insert(it, child);
+       m_model->endInsertRows();
+       return true;
 }
-*/
-bool Node::updated(Operation type)
-{
-       Q_UNUSED(type);
 
+void Node::updated(const Data * const oldData)
+{
        const int r = row();
        const QModelIndex parentIndex(m_model->index(parent()));
        emit m_model->dataChanged(m_model->index(r, 0, parentIndex),
                                                          m_model->index(r, columnCount() - 1, parentIndex));
 
-       return false;
+       if (m_parent && m_parent->data())
+               m_parent->data()->type()->childUpdated(this, oldData);
+
 }
 
 } // namespace DynamicModel
index ea25af05f8484ca2fcf7f60769ae9c3fd149fa6c..fbe8d37f93c46c4c02b3559ba9e0044b7bbd6730 100644 (file)
@@ -7,6 +7,8 @@
 #include <QVariant>
 #include <QList>
 
+class QSqlQuery;
+
 namespace LocalMyList {
 namespace DynamicModel {
 
@@ -23,7 +25,6 @@ public:
        ~Node();
 
        DataType *childDataType() const;
-       void setChildDataType(DataType *dataType);
 
        bool isRoot() const { return !m_parent; }
 
@@ -34,11 +35,13 @@ public:
        int columnCount() const;
        int row() const;
        bool hasChildren() const;
+       Model *model() const;
 
        // Data
        int id() const;
        QVariant data(int column, int role) const;
        Data *data() const;
+       bool setData(int column, const QVariant &data, int role);
        int totalRowCount() const;
 
        bool canFetchMore() const;
@@ -46,15 +49,20 @@ public:
        void fetchComplete();
 
        // Changes
-       void childAdded(Data *newData);
-       bool updated(Operation type);
+       Node *findParentOfNewEntry(const QVariantList &primaryValues);
+       bool childAdded(int id, DataType *dataType);
+       void updated(const Data * const oldData);
        MoveType moveChild(Node *child, Operation type);
+       void childDeleted(Node *child);
 
        // Misc
-       int depth() const;
+       int level() const;
+
+       Node *findChildByPrimaryValue(const QVariant &primaryValue);
 
 private:
        NodeFactory childNodeFactory();
+       void bindValues(QSqlQuery &query) const;
 
        Node *m_parent;
        Model *m_model;
@@ -62,7 +70,8 @@ private:
        int m_totalRowCount;
 
        Data *m_data;
-       DataType *m_childType;
+
+       static const int LIMIT = 400;
 };
 
 } // namespace DynamicModel
diff --git a/localmylist/dynamicmodel/queryparser.cpp b/localmylist/dynamicmodel/queryparser.cpp
new file mode 100644 (file)
index 0000000..de87e9d
--- /dev/null
@@ -0,0 +1,575 @@
+#include "dynamicmodel/queryparser.h"\r
+\r
+#include <QStringList>\r
+#include <QSet>\r
+\r
+#include "dynamicmodel/datatype.h"\r
+#include "dynamicmodel/typerelation.h"\r
+\r
+#include <QDebug>\r
+\r
+namespace LocalMyList {\r
+namespace DynamicModel {\r
+\r
+// TODO this data has to come from the data model\r
+namespace {\r
+const QMap<QString, QStringList> table_columns = []() {\r
+       QMap<QString, QStringList> r;\r
+       r["anime"] = QStringList()\r
+               << "aid"\r
+               << "entry_added"\r
+               << "anidb_update"\r
+               << "entry_update"\r
+               << "my_update"\r
+               << "title_english"\r
+               << "title_romaji"\r
+               << "title_kanji"\r
+               << "description"\r
+               << "year"\r
+               << "start_date"\r
+               << "end_date"\r
+               << "type"\r
+               << "total_episode_count"\r
+               << "highest_epno"\r
+               << "rating"\r
+               << "votes"\r
+               << "temp_rating"\r
+               << "temp_votes"\r
+               << "my_vote"\r
+               << "my_vote_date"\r
+               << "my_temp_vote"\r
+               << "my_temp_vote_date";\r
+       r["episode"] = QStringList()\r
+               << "eid"\r
+               << "aid"\r
+               << "entry_added"\r
+               << "anidb_update"\r
+               << "entry_update"\r
+               << "my_update"\r
+               << "epno"\r
+               << "title_english"\r
+               << "title_romaji"\r
+               << "title_kanji"\r
+               << "length"\r
+               << "airdate"\r
+               << "state"\r
+               << "type"\r
+               << "recap"\r
+               << "rating"\r
+               << "votes"\r
+               << "my_vote"\r
+               << "my_vote_date";\r
+       r["file"] = QStringList()\r
+               << "fid"\r
+               << "eid"\r
+               << "aid"\r
+               << "gid"\r
+               << "lid"\r
+               << "entry_added"\r
+               << "anidb_update"\r
+               << "entry_update"\r
+               << "my_update"\r
+               << "ed2k"\r
+               << "size"\r
+               << "length"\r
+               << "extension"\r
+               << "group_name"\r
+               << "group_name_short"\r
+               << "crc"\r
+               << "release_date"\r
+               << "version"\r
+               << "censored"\r
+               << "deprecated"\r
+               << "source"\r
+               << "quality"\r
+               << "resolution"\r
+               << "video_codec"\r
+               << "audio_codec"\r
+               << "audio_language"\r
+               << "subtitle_language"\r
+               << "aspect_ratio"\r
+               << "my_watched"\r
+               << "my_state"\r
+               << "my_file_state"\r
+               << "my_storage"\r
+               << "my_source"\r
+               << "my_other";\r
+       return r;\r
+}();\r
+\r
+const QString ellipsisPart{"..."};\r
+\r
+const QList<QString> ellipsisParts = []() {\r
+       QList<QString> ret;\r
+       ret << "anime"\r
+               << "episode"\r
+               << "file"\r
+               << "file_location";\r
+       return ret;\r
+}();\r
+}\r
+\r
+QDebug operator<<(QDebug dbg, const QueryParser::Level &l)\r
+{\r
+       if (l.column.isEmpty())\r
+               dbg << QString("[%1:%2]").arg(l.type).arg(l.table);\r
+       else\r
+               dbg << QString("[%1:%2.%3]").arg(l.type).arg(l.table).arg(l.column);\r
+       return dbg;\r
+}\r
+\r
+QueryParser::QueryParser(DataModel *dataModel) : m_dataModel{dataModel}, m_valid{false}\r
+{\r
+}\r
+\r
+bool QueryParser::parse(const QString &rawPath)\r
+{\r
+       static const QString emptyString{};\r
+\r
+       if (!m_dataModel)\r
+       {\r
+               m_errorString = QObject::tr("QueryParser needs a DataModel");\r
+               m_valid = false;\r
+               return m_valid;\r
+       }\r
+\r
+       m_errorString = QString{};\r
+\r
+       m_queryString = rawPath;\r
+       QStringList parts = m_queryString.split(QChar('/'), QString::SkipEmptyParts);\r
+       qDebug() << "parse " << parts;\r
+\r
+       if (!parts.length())\r
+               parts << "...";\r
+\r
+       m_levels.clear();\r
+       m_levels.reserve(parts.length());\r
+\r
+       for (int i = 0; i < parts.length(); ++i)\r
+       {\r
+               Level currentLevel;\r
+\r
+               if (parts[i] == ellipsisPart)\r
+               {\r
+                       if (i != parts.length() - 1)\r
+                       {\r
+                               m_errorString = QObject::tr("Ellipsis can only be the last element of the Query");\r
+                               m_valid = false;\r
+                               return m_valid;\r
+                       }\r
+\r
+                       //parts.removeLast();\r
+                       int startIndex = 0;\r
+                       if (parts.length() > 1)\r
+                       {\r
+                               const Level &lastLevel = level(parts.length() - 2);\r
+                               for (int j = 0; j < ellipsisParts.length(); ++j)\r
+                               {\r
+                                       if (ellipsisParts[j] == lastLevel.table)\r
+                                       {\r
+                                               startIndex = j + (lastLevel.type != ColumnEntry);\r
+                                               break;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       parts.reserve(parts.length() + ellipsisParts.length() - startIndex);\r
+                       if (startIndex < ellipsisParts.length())\r
+                       {\r
+                               parts[i] = ellipsisParts[startIndex];\r
+                               for (int j = startIndex + 1; j < ellipsisParts.length(); ++j)\r
+                               {\r
+                                       parts << ellipsisParts[j];\r
+                               }\r
+                       }\r
+                       else\r
+                       {\r
+                               parts.removeLast();\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               const QString &part = parts[i];\r
+\r
+               const QStringList tableColumn = part.split(QChar('.'));\r
+               const QString &table = tableColumn[0];\r
+               const QString &column = tableColumn.size() > 1 ? tableColumn[1] : emptyString;\r
+\r
+//             qDebug() << "----------------------- Iteration" << i << "-----------------------";\r
+               qDebug() << "part(" << part.length() << ") =" << table << "(" << column << ")";\r
+\r
+               if (!m_dataModel->hasDataType(table))\r
+               {\r
+                       m_errorString = QObject::tr("Table \"%1\" does not exist.").arg(table);\r
+                       m_valid = false;\r
+                       return m_valid;\r
+               }\r
+               else\r
+               {\r
+                       currentLevel.table = table;\r
+                       currentLevel.tableAlias = m_dataModel->dataType(table)->alias();\r
+                       currentLevel.type = TableEntry;\r
+               }\r
+\r
+               if (!column.isEmpty())\r
+               {\r
+                       if (!table_columns[currentLevel.table].contains(column))\r
+                       {\r
+                               m_errorString = QObject::tr("Column %1 does not exist in table %2.")\r
+                                               .arg(column).arg(table);\r
+                               m_valid = false;\r
+                               return m_valid;\r
+                       }\r
+                       currentLevel.column = column;\r
+                       currentLevel.type = ColumnEntry;\r
+               }\r
+\r
+               if (i\r
+                       && m_levels.last().table != currentLevel.table\r
+                       && !m_dataModel->hasTypeRelation(m_levels.last().table, currentLevel.table))\r
+               {\r
+                       m_errorString = QObject::tr("No relation defined between table %1 and table %2.")\r
+                                       .arg(m_levels.last().table).arg(currentLevel.table);\r
+                       m_valid = false;\r
+                       return m_valid;\r
+               }\r
+\r
+               m_levels.push_back(currentLevel);\r
+       }\r
+\r
+       qDebug() << m_levels;\r
+\r
+       m_valid = true;\r
+       return m_valid;\r
+}\r
+\r
+\r
+QString QueryParser::buildSql(int currentLevel) const\r
+{\r
+       if (!m_valid) return {};\r
+       resetPlaceHolderUse();\r
+\r
+       const Level &lastLevel = level(currentLevel);\r
+       const DataType *dataType = m_dataModel->dataType(lastLevel.table);\r
+\r
+       QString columns = QString("(%1)").arg(buildChildCountSql(currentLevel));\r
+\r
+       if (!lastLevel.column.isEmpty())\r
+       {\r
+               columns += QString(", %2.%1")\r
+                               .arg(lastLevel.column).arg(dataType->alias());\r
+       }\r
+       else\r
+       {\r
+               QString additionalColumns = dataType->additionalColumns();\r
+               if (!additionalColumns.isEmpty())\r
+               {\r
+                       columns += QString(", %1").arg(additionalColumns);\r
+               }\r
+       }\r
+\r
+       QString ret = buildSelect(currentLevel, columns, true, true);\r
+\r
+       if (lastLevel.type == ColumnEntry)\r
+       {\r
+               QString column = QString("%2.%1")\r
+                               .arg(lastLevel.column).arg(dataType->alias());\r
+               ret += QString("\n\tGROUP BY %1\n\tORDER BY %1")\r
+                               .arg(column);\r
+       }\r
+       else if (!dataType->orderBy().isEmpty())\r
+       {\r
+               ret += QString("\n\tORDER BY %1").arg(dataType->orderBy());\r
+       }\r
+\r
+       ret += QString("\n\tLIMIT :limit\n\tOFFSET :offset\n");\r
+\r
+       qDebug() << "================================================== sql ========================================================";\r
+       qDebug() << ret;\r
+       qDebug() << "===============================================================================================================";\r
+       return ret;\r
+}\r
+\r
+QString QueryParser::buildCountSql(int currentLevel) const\r
+{\r
+       if (!m_valid) return {};\r
+       resetPlaceHolderUse();\r
+       QString ret = buildChildCountSql(currentLevel);\r
+\r
+       qDebug() << "============================================ child count sql ==================================================";\r
+       qDebug() << ret;\r
+       qDebug() << "===============================================================================================================";\r
+       return ret;\r
+}\r
+\r
+QString QueryParser::buildEntrySql(int currentLevel) const\r
+{\r
+       if (!m_valid) return {};\r
+       resetPlaceHolderUse();\r
+       QString ret = buildEntrySqlInternal(currentLevel);\r
+\r
+       qDebug() << "=============================================== entry sql =====================================================";\r
+       qDebug() << ret;\r
+       qDebug() << "===============================================================================================================";\r
+       return ret;\r
+}\r
+\r
+QString QueryParser::buildPrimaryValuesSql(int maxLevel) const\r
+{\r
+       if (!m_valid) return {};\r
+       resetPlaceHolderUse();\r
+\r
+       QStringList columnList;\r
+\r
+       for (int i = 0; i < maxLevel; ++i)\r
+       {\r
+               const DataType *type = dataType(i);\r
+               QString alias = level(i).type == ColumnEntry ? level(i).tableAlias : type->alias();\r
+               columnList << valueColumn(i, alias);\r
+       }\r
+\r
+       if (!columnList.length())\r
+       {\r
+               return "SELECT 0";\r
+       }\r
+\r
+       QString columns = columnList.join(", ");\r
+\r
+       QString ret = buildSelect(maxLevel, columns, false, false);\r
+\r
+       const DataType *maxLevelDataType = dataType(maxLevel);\r
+       ret += QString{"\n\tWHERE %1 = :id"}.arg(valueColumn(maxLevel, maxLevelDataType->alias()));\r
+\r
+       qDebug() << "=========================================== primaryValues sql =================================================";\r
+       qDebug() << ret;\r
+       qDebug() << "===============================================================================================================";\r
+       return ret;\r
+}\r
+\r
+void QueryParser::bindValue(QSqlQuery &query, Data *data, int currentLevel) const\r
+{\r
+       Q_ASSERT_X(m_valid, "dynamicmodel/query", "Bind value for invalid query");\r
+       Q_ASSERT_X(currentLevel >= -1 && m_levels.count() >= currentLevel, "dynamicmodel/query", "Bind value for invalid level");\r
+       if (!data) return;\r
+\r
+       qDebug() << "binding" << data->primaryValue() << "on level" << currentLevel;\r
+       QRegExp rx(QString(":level_%1_value_([0-9]+)").arg(currentLevel));\r
+       const QString sqlQuery = query.lastQuery();\r
+       int pos = 0;\r
+       while ((pos = rx.indexIn(sqlQuery, pos)) != -1)\r
+       {\r
+               qDebug() << "WWWWW0" << placeHolder(currentLevel, rx.cap(1).toInt());\r
+               query.bindValue(placeHolder(currentLevel, rx.cap(1).toInt()), data->primaryValue());\r
+               pos += rx.matchedLength();\r
+       }\r
+}\r
+\r
+QString QueryParser::buildChildCountSql(int currentLevel, const QString &aliasSuffix) const\r
+{\r
+       if (currentLevel >= levels() - 1)\r
+               return "0";\r
+\r
+       const Level &nextLevel = level(currentLevel + 1);\r
+       const DataType *nextLeveldataType = m_dataModel->dataType(nextLevel.table);\r
+\r
+       QString countColumn = QString{"count(DISTINCT %1)"}\r
+                       .arg(valueColumn(currentLevel + 1, nextLeveldataType->alias() + aliasSuffix));\r
+\r
+       QString query = buildSelect(currentLevel + 1, countColumn, false, true, aliasSuffix);\r
+       if (currentLevel >= 0)\r
+       {\r
+               const Level &lastLevel = level(currentLevel);\r
+               const DataType *lastLeveldataType = m_dataModel->dataType(lastLevel.table);\r
+               query.replace(currentPlaceHolder(currentLevel), valueColumn(currentLevel, lastLeveldataType->alias() /*+ aliasSuffix*/));\r
+       }\r
+       return query;\r
+}\r
+\r
+QString QueryParser::buildEntrySqlInternal(int currentLevel) const\r
+{\r
+       const Level &lastLevel = level(currentLevel);\r
+       const DataType *dataType = m_dataModel->dataType(lastLevel.table);\r
+\r
+       QString columns = QString("(%1)").arg(buildChildCountSql(currentLevel));\r
+\r
+       if (!lastLevel.column.isEmpty())\r
+       {\r
+               columns += QString(", %2.%1")\r
+                               .arg(lastLevel.column).arg(dataType->alias());\r
+       }\r
+       else\r
+       {\r
+               QString additionalColumns = dataType->additionalColumns();\r
+               if (!additionalColumns.isEmpty())\r
+               {\r
+                       columns += QString(", %1").arg(additionalColumns);\r
+               }\r
+       }\r
+\r
+       QString ret = buildSelect(currentLevel, columns, true, true);\r
+       ret += QString{"\n\t\tWHERE %1.%2 = :id"}\r
+                       .arg(dataType->alias())\r
+                       .arg(dataType->primaryKeyName());\r
+       return ret;\r
+}\r
+\r
+QString QueryParser::buildSelect(int currentLevel, const QString &columns, bool willRequireAdditionalJoins, bool includeConditions, const QString &aliasSuffix) const\r
+{\r
+       const Level &lastLevel = level(currentLevel);\r
+       const DataType *dataType = m_dataModel->dataType(lastLevel.table);\r
+       const QString joins = buildJoins(currentLevel - 1, willRequireAdditionalJoins, includeConditions, aliasSuffix);\r
+       return QString("\nSELECT DISTINCT %4 FROM %1 %2%3")\r
+                       .arg(lastLevel.table).arg(dataType->alias() + aliasSuffix).arg(joins).arg(columns);\r
+}\r
+\r
+QString QueryParser::buildJoins(int currentLevel, bool willRequireAdditionalJoins, bool includeConditions, const QString &aliasSuffix) const\r
+{\r
+       QMap<QString, QStringList> conditions;\r
+\r
+       // main table is the one in FROM\r
+       const QString &mainTable = level(currentLevel + 1).table;\r
+\r
+       for (int i = currentLevel; i >= 0; --i)\r
+       {\r
+               const QString &nextTable = level(i + 1).table;\r
+               const QString &table = level(i).table;\r
+\r
+               auto it = conditions.find(table);\r
+               const DataType *dataType = m_dataModel->dataType(level(i).table);\r
+\r
+               if (it == conditions.end())\r
+               {\r
+                       it = conditions.insert(table, QStringList());\r
+                       if (table != nextTable)\r
+                       {\r
+                               const TypeRelation *rel = m_dataModel->typeRelation(table, nextTable);\r
+                               const DataType *nextDataType = m_dataModel->dataType(rel->destinationType());\r
+\r
+                               *it << rel->joinCondition(dataType->alias() + aliasSuffix, nextDataType->alias() + aliasSuffix);\r
+                       }\r
+               }\r
+\r
+               if (!includeConditions)\r
+                       continue;\r
+\r
+               if (level(i).type == ColumnEntry)\r
+               {\r
+                       *it << QString("%1.%2 = %3")\r
+                                       .arg(dataType->alias() + aliasSuffix).arg(level(i).column).arg(nextPlaceHolder(i));\r
+               }\r
+               else\r
+               {\r
+                       *it << QString("%1.%2 = %3")\r
+                                       .arg(dataType->alias() + aliasSuffix).arg(dataType->primaryKeyName()).arg(nextPlaceHolder(i));\r
+               }\r
+       }\r
+       qDebug() << conditions;\r
+\r
+       QString ret;\r
+       QSet<QString> addedTables;\r
+       addedTables.insert(mainTable);\r
+       for (int i = currentLevel; i >= 0; --i)\r
+       {\r
+               const Level &l = level(i);\r
+               if (!addedTables.contains(l.table) && conditions.contains(l.table))\r
+               {\r
+                       const DataType *dataType = m_dataModel->dataType(l.table);\r
+                       ret += QString("\n\tJOIN %1 %2 ON %3\n").arg(l.table).arg(dataType->alias() + aliasSuffix).arg(conditions[l.table].join("\n\t\tAND "));\r
+                       addedTables.insert(l.table);\r
+               }\r
+       }\r
+\r
+       if (willRequireAdditionalJoins)\r
+       {\r
+               const QString additionalJoins = m_dataModel->dataType(mainTable)->additionalJoins();\r
+               if (!additionalJoins.isEmpty())\r
+                       ret += QString("\n\t%1\n").arg(additionalJoins);\r
+       }\r
+\r
+       if (conditions.contains(mainTable))\r
+       {\r
+               ret += QString("\n\tWHERE %1\n").arg(conditions[mainTable].join("\n\t\tAND "));\r
+       }\r
+       return ret;\r
+}\r
+\r
+QString QueryParser::valueColumn(int currentLevel, const QString &alias) const\r
+{\r
+       const Level &lastLevel = level(currentLevel);\r
+       QString column = QString{"%2.%1"};\r
+       if (lastLevel.type == ColumnEntry)\r
+       {\r
+               return column\r
+                               .arg(lastLevel.column).arg(alias);\r
+       }\r
+       const DataType *dataType = m_dataModel->dataType(lastLevel.table);\r
+       return column\r
+                       .arg(dataType->primaryKeyName()).arg(alias);\r
+}\r
+\r
+QString QueryParser::currentPlaceHolder(int currentLevel) const\r
+{\r
+       return placeHolder(currentLevel, m_placeholderUse[currentLevel]);\r
+}\r
+\r
+QString QueryParser::nextPlaceHolder(int currentLevel) const\r
+{\r
+       return placeHolder(currentLevel, ++m_placeholderUse[currentLevel]);\r
+}\r
+\r
+QString QueryParser::placeHolder(int currentLevel, int i) const\r
+{\r
+       return QString(":level_%1_value_%2").arg(currentLevel).arg(i);\r
+}\r
+\r
+void QueryParser::resetPlaceHolderUse() const\r
+{\r
+       m_placeholderUse.fill(0, levels());\r
+}\r
+\r
+bool QueryParser::isValid() const\r
+{\r
+       return m_valid;\r
+}\r
+\r
+int QueryParser::levels() const\r
+{\r
+       return m_levels.count();\r
+}\r
+\r
+const QueryParser::Level &QueryParser::level(int i) const\r
+{\r
+       Q_ASSERT_X(i >= 0 && m_levels.count() >= i, "dynamicmodel/query", "Requestesd invlaid level index");\r
+       return m_levels[i];\r
+}\r
+\r
+QString QueryParser::query() const\r
+{\r
+       return m_queryString;\r
+}\r
+\r
+QString QueryParser::errorString() const\r
+{\r
+       return m_errorString;\r
+}\r
+\r
+DataModel *QueryParser::dataModel() const\r
+{\r
+       return m_dataModel;\r
+}\r
+\r
+DataType *QueryParser::dataType(int currentLevel) const\r
+{\r
+       const Level l = level(currentLevel);\r
+       if (l.type == ColumnEntry)\r
+               return m_dataModel->dataType("column");\r
+       return m_dataModel->dataType(l.table);\r
+}\r
+\r
+bool operator ==(const QueryParser &a, const QueryParser &b)\r
+{\r
+       return a.m_dataModel == b.m_dataModel && a.m_queryString == b.m_queryString;\r
+}\r
+\r
+} // namespace DynamicModel\r
+} // namespace LocalMyList\r
diff --git a/localmylist/dynamicmodel/queryparser.h b/localmylist/dynamicmodel/queryparser.h
new file mode 100644 (file)
index 0000000..8c9f189
--- /dev/null
@@ -0,0 +1,79 @@
+#ifndef QUERYPARSER_H\r
+#define QUERYPARSER_H\r
+\r
+#include "localmylist_global.h"\r
+#include <QString>\r
+#include <QVector>\r
+#include <QSqlQuery>\r
+#include "dynamicmodel/datamodel.h"\r
+#include "dynamicmodel/data.h"\r
+\r
+namespace LocalMyList {\r
+namespace DynamicModel {\r
+\r
+// TODO split this class into a (model) Query Parser and a (SQL)Query builder\r
+class LOCALMYLISTSHARED_EXPORT QueryParser\r
+{\r
+public:\r
+       enum EntryType {\r
+               TableEntry,\r
+               ColumnEntry,\r
+       };\r
+\r
+       struct Level {\r
+               EntryType type;\r
+               QString table;\r
+               QString column;\r
+               QString tableAlias;\r
+       };\r
+\r
+       QueryParser(DataModel *dataModel = 0);\r
+\r
+       bool parse(const QString &rawPath);\r
+\r
+       QString buildSql(int currentLevel) const;\r
+       QString buildCountSql(int currentLevel) const;\r
+       QString buildEntrySql(int currentLevel) const;\r
+       QString buildPrimaryValuesSql(int maxLevel) const;\r
+       void bindValue(QSqlQuery &query, Data *data, int currentLevel) const;\r
+\r
+       bool isValid() const;\r
+       int levels() const;\r
+       const Level &level(int i) const;\r
+\r
+       QString query() const;\r
+       QString errorString() const;\r
+       DataModel *dataModel() const;\r
+       DataType *dataType(int currentLevel) const;\r
+\r
+       friend bool operator ==(const QueryParser& a, const QueryParser& b);\r
+\r
+private:\r
+       QString buildChildCountSql(int currentLevel, const QString &aliasSuffix = "2") const;\r
+       QString buildEntrySqlInternal(int currentLevel) const;\r
+       QString buildJoins(int currentLevel, bool willRequireAdditionalJoins, bool includeConditions, const QString &aliasSuffix = QString{}) const;\r
+       QString buildSelect(int currentLevel, const QString &columns, bool willRequireAdditionalJoins, bool includeConditions, const QString &aliasSuffix = QString{}) const;\r
+\r
+       QString valueColumn(int currentLevel, const QString &alias) const;\r
+       QString currentPlaceHolder(int currentLevel) const;\r
+       QString nextPlaceHolder(int currentLevel) const;\r
+       QString placeHolder(int currentLevel, int i) const;\r
+       void resetPlaceHolderUse() const;\r
+\r
+       bool m_valid;\r
+       QString m_queryString;\r
+       QString m_errorString;\r
+       QVector<Level> m_levels;\r
+       mutable QVector<int> m_placeholderUse;\r
+       DataModel *m_dataModel;\r
+\r
+};\r
+\r
+\r
+QDebug operator<<(QDebug dbg, const QueryParser::Level &l);\r
+\r
+} // namespace DynamicModel\r
+} // namespace LocalMyList\r
+\r
+\r
+#endif // QUERYPARSER_H\r
index 5889df6c13ba694f5a10b6af49c459adaea59b8a..6aa5c9fbc2890deb35a727978349477518af09d9 100644 (file)
@@ -1,12 +1,12 @@
 #include "typerelation.h"
 
-#include "../mylist.h"
-#include "../database.h"
-#include "../databaseclasses.h"
-#include "node.h"
-#include "datatype.h"
-#include "data.h"
-#include "types.h"
+#include "dynamicmodel/node.h"
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/types.h"
+#include "mylist.h"
+#include "database.h"
+#include "databaseclasses.h"
 
 #include <QDebug>
 
@@ -17,531 +17,41 @@ TypeRelation::TypeRelation(QObject *parent) : QObject(parent), m_dataType(0)
 {
 }
 
-IdList TypeRelation::getParents(int id)
-{
-       Q_UNUSED(id);
-       return IdList();
-}
+QString TypeRelation::joinCondition(const QString &, const QString &) const { return {}; }
+
 
 DataType *TypeRelation::dataType() const
 {
        return m_dataType;
 }
 
-QString TypeRelation::childRowCountQuery(DataType *type) const
-{
-       static const QString zeroQuery("0");
-
-       if (!type)
-               return zeroQuery;
-
-       TypeRelation *rel = dataType()->model()->typeRelation(destinationType(), type->name());
-
-       qDebug() << "relation" << rel->sourceType() << rel->destinationType();
-       if (!rel)
-               return zeroQuery;
-
-       return rel->rowCountQuery();
-}
-
 // ===========================================================================
 
-RootAnimeRelation::RootAnimeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString RootAnimeRelation::sourceType() const
-{
-       return QString();
-}
-
-QString RootAnimeRelation::destinationType() const
-{
-       return "anime";
-}
-
-NodeList RootAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       Q_UNUSED(parent);
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, "
-       "%1 "
-       "ORDER BY title_romaji ASC "
-       "LIMIT :limit "
-       "OFFSET :offset ")
-       .arg(dataType()->baseQuery())
-       .arg(childRowCountQuery(rowCountType)));
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
 
-       return newItems;
-}
-
-QString RootAnimeRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(aid) FROM anime)";
-}
-
-// =================================================
-
-RootAnimeTitleRelation::RootAnimeTitleRelation(QObject *parent) : TypeRelation(parent)
+ForeignKeyRelation::ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, const QString &fk, QObject *parent)
+       : TypeRelation{parent}, m_left{left}, m_right{right}, m_pk{pk}, m_fk{fk}
 {
 }
 
-QString RootAnimeTitleRelation::sourceType() const
+ForeignKeyRelation::ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, QObject *parent)
+       : TypeRelation{parent}, m_left{left}, m_right{right}, m_pk{pk}, m_fk{pk}
 {
-       return QString();
 }
 
-QString RootAnimeTitleRelation::destinationType() const
+QString ForeignKeyRelation::sourceType() const
 {
-       return "anime_title";
+       return m_left;
 }
 
-NodeList RootAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
+QString ForeignKeyRelation::destinationType() const
 {
-       Q_UNUSED(parent);
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, %1 "
-       "       ORDER BY title ASC "
-       "LIMIT :limit "
-       "OFFSET :offset ")
-       .arg(dataType()->baseQuery())
-       .arg(childRowCountQuery(rowCountType)));
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-
-       return newItems;
-}
-
-QString RootAnimeTitleRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(title_id) FROM anime_title)";
-}
-
-// =================================================
-
-
-RootEpisodeRelation::RootEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString RootEpisodeRelation::sourceType() const
-{
-       return QString();
-}
-
-QString RootEpisodeRelation::destinationType() const
-{
-       return "episode";
-}
-
-NodeList RootEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       Q_UNUSED(parent)
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT "
-       "       %2, "
-       "       %1 "
-       "       ORDER BY et.ordering ASC, e.epno ASC "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-
-       return newItems;
-}
-
-QString RootEpisodeRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(eid) FROM episode)";
-}
-
-// =================================================
-
-AnimeEpisodeRelation::AnimeEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeEpisodeRelation::sourceType() const
-{
-       return "anime";
-}
-
-QString AnimeEpisodeRelation::destinationType() const
-{
-       return "episode";
-}
-
-NodeList AnimeEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT "
-       "       %2, "
-       "       %1 "
-       "       WHERE e.aid = :aid "
-       "       ORDER BY et.ordering ASC, e.epno ASC "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":aid", parent->id());
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-
-       return newItems;
-}
-
-QString AnimeEpisodeRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(eid) FROM episode WHERE aid = a.aid)";
-}
-
-IdList AnimeEpisodeRelation::getParents(int id)
-{
-       QSqlQuery &q = MyList::instance()->database()->prepare(
-       "SELECT e.aid FROM episode e WHERE e.eid = :eid");
-       q.bindValue(":eid", id);
-
-       IdList ret;
-
-       if (!q.exec())
-               return ret;
-
-       while (q.next())
-               ret << q.value(0).toInt();
-
-       q.finish();
-
-       return ret;
-}
-
-// =================================================
-
-AnimeAnimeTitleRelation::AnimeAnimeTitleRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeAnimeTitleRelation::sourceType() const
-{
-       return "anime";
-}
-
-QString AnimeAnimeTitleRelation::destinationType() const
-{
-       return "anime_title";
-}
-
-NodeList AnimeAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT "
-       "       %2, "
-       "       %1 "
-       "       WHERE at.aid = :aid "
-       "       ORDER BY at.type ASC, at.title ASC "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":aid", parent->id());
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-
-       return newItems;
-}
-
-QString AnimeAnimeTitleRelation::rowCountQuery() const
-{
-       return "(SELECT count(title_id) FROM anime_title WHERE aid = a.aid)";
-}
-
-// =================================================
-
-EpisodeFileRelation::EpisodeFileRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString EpisodeFileRelation::sourceType() const
-{
-       return "episode";
-}
-
-QString EpisodeFileRelation::destinationType() const
-{
-       return "file";
-}
-
-NodeList EpisodeFileRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, %1 "
-       "       FROM file f "
-       "       WHERE f.eid = :eida "
-       "UNION "
-       "SELECT %2, %1 FROM file f "
-       "       JOIN file_episode_rel fer ON (fer.fid = f.fid) "
-       "       WHERE fer.eid = :eidb ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":eida", parent->id());
-       q.bindValue(":eidb", parent->id());
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-       return newItems;
-}
-
-QString EpisodeFileRelation::rowCountQuery() const
-{
-       return
-       "       (SELECT COUNT(fid) "
-       "               FROM ( "
-       "                       SELECT fid "
-       "                               FROM file "
-       "                               WHERE eid = e.eid "
-       "                       UNION "
-       "                       SELECT f.fid FROM file f "
-       "                               JOIN file_episode_rel fer ON (fer.fid = f.fid) "
-       "                               WHERE fer.eid = e.eid) sq) ";
-}
-
-// =================================================
-
-FileFileLocationRelation::FileFileLocationRelation(QObject *parent) : TypeRelation(parent)
-{
-
-}
-
-QString FileFileLocationRelation::sourceType() const
-{
-       return "file";
-}
-
-QString FileFileLocationRelation::destinationType() const
-{
-       return "file_location";
-}
-
-NodeList FileFileLocationRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, %1 "
-       "       WHERE fl.fid = :fid "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":fid", parent->id());
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-       return newItems;
-}
-
-QString FileFileLocationRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(location_id) FROM file_location WHERE fid = f.fid)";
-}
-
-// =================================================
-
-AnimeTitleAnimeRelation::AnimeTitleAnimeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeTitleAnimeRelation::sourceType() const
-{
-       return "anime_title";
-}
-
-QString AnimeTitleAnimeRelation::destinationType() const
-{
-       return "anime";
-}
-
-NodeList AnimeTitleAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       int aid = static_cast<AnimeTitleData *>(parent)->animeTitleData.aid;
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, %1 "
-       "       WHERE a.aid = :aid "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":aid", aid);
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-       return newItems;
-}
-
-QString AnimeTitleAnimeRelation::rowCountQuery() const
-{
-       return "(SELECT COUNT(aid) FROM anime WHERE aid = at.aid)";
-}
-
-// =================================================
-
-AnimeTitleEpisodeRelation::AnimeTitleEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeTitleEpisodeRelation::sourceType() const
-{
-       return "anime_title";
-}
-
-QString AnimeTitleEpisodeRelation::destinationType() const
-{
-       return "episode";
-}
-
-NodeList AnimeTitleEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
-       if (!parent)
-               return NodeList();
-
-       int aid = static_cast<AnimeTitleData *>(parent)->animeTitleData.aid;
-
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT %2, %1 "
-       "       WHERE e.aid = :aid "
-       "       LIMIT :limit "
-       "       OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
-       q.bindValue(":aid", aid);
-       q.bindValue(":limit", LIMIT);
-       q.bindValue(":offset", offset);
-
-       if (!q.exec())
-               return NodeList();
-
-       NodeList newItems;
-       QSqlResultIterator it(q);
-       while (it.next())
-       {
-               int totalRowCount = it.value(0).toInt();
-               Data *data = dataType()->readEntry(it);
-               auto node = nodeFactory(data, totalRowCount);
-               newItems << node;
-       }
-       return newItems;
+       return m_right;
 }
 
-QString AnimeTitleEpisodeRelation::rowCountQuery() const
+QString ForeignKeyRelation::joinCondition(const QString &leftAlias, const QString &rightAlias) const
 {
-       return "(SELECT COUNT(eid) FROM episode WHERE aid = at.aid)";
+       return QString{"%2.%3 = %1.%4"}
+               .arg(rightAlias).arg(leftAlias).arg(m_pk).arg(m_fk);
 }
 
 } // namespace DynamicModel
index 63b6263b874e6afb30fe70d179f82a17652b13c7..ba612e7f423a6e12e4fc37d34c365e76616f3aba 100644 (file)
@@ -22,173 +22,31 @@ public:
 
        virtual QString sourceType() const = 0;
        virtual QString destinationType() const = 0;
-
-       virtual NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) = 0;
-       virtual QString rowCountQuery() const = 0;
-       virtual IdList getParents(int id);
+       virtual QString joinCondition(const QString &leftAlias, const QString &rightAlias) const;
 
        DataType *dataType() const;
 
-protected:
-       QString childRowCountQuery(DataType *type) const;
-
-       static const int LIMIT = 400;
-
 private:
        DataType *m_dataType;
 };
 
 // =========================================================================================================
 
-class LOCALMYLISTSHARED_EXPORT RootAnimeRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       RootAnimeRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT RootAnimeTitleRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       RootAnimeTitleRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT RootEpisodeRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       RootEpisodeRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeEpisodeRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       AnimeEpisodeRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-       IdList getParents(int id);
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeAnimeTitleRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       AnimeAnimeTitleRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT EpisodeFileRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       EpisodeFileRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-/*
-class LOCALMYLISTSHARED_EXPORT EpisodeFileLocationRelation : public TypeRelation
+class LOCALMYLISTSHARED_EXPORT ForeignKeyRelation : public TypeRelation
 {
-       Q_OBJECT
 public:
-       EpisodeFileLocationRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
+       ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, const QString &fk, QObject *parent = 0);
+       ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, QObject *parent = 0);
 
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-*/
+       virtual QString sourceType() const override;
+       virtual QString destinationType() const override;
+       QString joinCondition(const QString &leftAlias, const QString &rightAlias) const override;
 
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT FileFileLocationRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       FileFileLocationRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeTitleAnimeRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       AnimeTitleAnimeRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeTitleEpisodeRelation : public TypeRelation
-{
-       Q_OBJECT
-public:
-       AnimeTitleEpisodeRelation(QObject *parent);
-
-       QString sourceType() const;
-       QString destinationType() const;
-
-       NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
-       QString rowCountQuery() const;
+private:
+       QString m_left;
+       QString m_right;
+       QString m_pk;
+       QString m_fk;
 };
 
 } // namespace DynamicModel
index df77a71b9570f87e6cf3b1821adad0b669546de1..0b82faebea4db859e56c31dc8f8e65780b34a427 100644 (file)
 #include "types.h"
 
-#include "../database.h"
-#include "../mylist.h"
-#include "datamodel.h"
-#include "typerelation.h"
-#include "data.h"
-#include "node.h"
+#include "dynamicmodel/datamodel.h"
+#include "dynamicmodel/typerelation.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/node.h"
+#include "database.h"
+#include "mylist.h"
 
 #include <QDebug>
 
 namespace LocalMyList {
 namespace DynamicModel {
 
-QString AnimeType::name() const
+QString ColumnType::name() const
 {
-       return "anime";
+       return "column";
 }
 
-QStringList AnimeType::availableChildRelations() const
+QString ColumnType::tableName() const
 {
-       return QStringList();
+       return {};
 }
 
-QString AnimeType::baseQuery() const
+QString ColumnType::alias() const
 {
-       return QString(
-       "               (SELECT COUNT(e.eid) "
-       "                       FROM episode e "
-       "                       WHERE e.aid = a.aid), "
-       "               (SELECT COUNT(DISTINCT eid) "
-       "                       FROM "
-       "                       (SELECT e.eid FROM episode e "
-       "                               JOIN file f ON (f.eid = e.eid) "
-       "                               WHERE e.aid = a.aid "
-       "                                       AND f.my_watched IS NOT NULL "
-       "                       UNION "
-       "                       SELECT e.eid FROM episode e "
-       "                               JOIN file_episode_rel fer ON fer.eid = e.eid "
-       "                               JOIN file f ON f.fid = fer.fid "
-       "                               WHERE e.aid = a.aid "
-       "                                       AND f.my_watched IS NOT NULL) sq), "
-       "               (SELECT CASE WHEN array_length(my_state_array, 1) > 1 THEN -1 ELSE my_state_array[1] END "
-       "                       FROM "
-       "                       (SELECT array_agg(my_state) my_state_array "
-       "                               FROM "
-       "                                       (SELECT my_state "
-       "                                               FROM file "
-       "                                               WHERE aid = a.aid "
-       "                                       UNION "
-       "                                       SELECT f.my_state "
-       "                                               FROM file f "
-       "                                               JOIN file_episode_rel fer ON (fer.fid = f.eid) "
-       "                                               JOIN episode e ON (e.eid = fer.eid AND e.aid = a.aid) "
-       "                                               ) AS sq) AS sq) AS my_state, "
-       "               %1 "
-       "       FROM anime a ")
-                       .arg(Database::animeFields());
+       return {};
+}
+
+QString ColumnType::primaryKeyName() const
+{
+       return {};
+}
+
+QString ColumnType::additionalColumns() const
+{
+       return {};
+}
+
+Data *ColumnType::readEntry(const SqlResultIteratorInterface &it)
+{
+       auto typedData = new ColumnData(this);
+       typedData->value = it.value(1);
+       return typedData;
 }
 
-int AnimeType::size() const
+// =============================================================================================================
+
+QString AnimeType::tableName() const
 {
-       return sizeHelper("anime", "aid");
+       return "anime";
+}
+
+QString AnimeType::alias() const
+{
+       return "a";
+}
+
+QString AnimeType::primaryKeyName() const
+{
+       return "aid";
+}
+
+QString AnimeType::additionalColumns() const
+{
+       return QString{R"(
+               (SELECT COUNT(e.eid)
+                       FROM episode e
+                       WHERE e.aid = a.aid
+                               AND e.type = ''),
+               (SELECT COUNT(e.eid)
+                       FROM episode e
+                       WHERE e.aid = a.aid
+                               AND e.type <> ''),
+               (SELECT COUNT(DISTINCT eid)
+                       FROM
+                       (SELECT e.eid FROM episode e
+                               JOIN file f ON (f.eid = e.eid)
+                               WHERE e.aid = a.aid
+                                       AND f.my_watched IS NOT NULL
+                                       AND e.type = ''
+                       UNION
+                       SELECT e.eid FROM episode e
+                               JOIN file_episode_rel fer ON fer.eid = e.eid
+                               JOIN file f ON f.fid = fer.fid
+                               WHERE e.aid = a.aid
+                                       AND f.my_watched IS NOT NULL
+                                       AND e.type = '') sq),
+               (SELECT COUNT(DISTINCT eid)
+                       FROM
+                       (SELECT e.eid FROM episode e
+                               JOIN file f ON (f.eid = e.eid)
+                               WHERE e.aid = a.aid
+                                       AND f.my_watched IS NOT NULL
+                                       AND e.type <> ''
+                       UNION
+                       SELECT e.eid FROM episode e
+                               JOIN file_episode_rel fer ON fer.eid = e.eid
+                               JOIN file f ON f.fid = fer.fid
+                               WHERE e.aid = a.aid
+                                       AND f.my_watched IS NOT NULL
+                                       AND e.type <> '') sq),
+               (SELECT CASE WHEN array_length(my_state_array, 1) > 1 THEN -1 ELSE my_state_array[1] END
+                       FROM
+                       (SELECT array_agg(my_state) my_state_array
+                               FROM
+                                       (SELECT my_state
+                                               FROM file
+                                               WHERE aid = a.aid
+                                       UNION
+                                       SELECT f.my_state
+                                               FROM file f
+                                               JOIN file_episode_rel fer ON (fer.fid = f.eid)
+                                               JOIN episode e ON (e.eid = fer.eid AND e.aid = a.aid)
+                                               ) AS sq) AS sq) AS my_state,
+               (SELECT string_agg(at.title, '''') -- Quotes are replaced by backticks in everything returned by anidb
+                       FROM anime_title at
+                               WHERE at.aid = a.aid AND at.language = 'en') AS alternate_titles,
+               %1
+                               )"}.arg(Database::animeFields());
+}
+
+QString AnimeType::orderBy() const
+{
+       return "a.title_romaji";
 }
 
 void AnimeType::registerd()
 {
+       connect(MyList::instance()->database(), SIGNAL(animeInsert(int)), this, SLOT(animeAdded(int)));
        connect(MyList::instance()->database(), SIGNAL(animeUpdate(int)), this, SLOT(animeUpdated(int)));
+       connect(MyList::instance()->database(), SIGNAL(animeDelete(int)), this, SLOT(animeDeleted(int)));
+
+       connect(MyList::instance()->database(), SIGNAL(fileInsert(int,int,int)), this, SLOT(fileAdded(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileDelete(int,int,int)), this, SLOT(fileDeleted(int,int,int)));
+
+       connect(MyList::instance()->database(), SIGNAL(episodeInsert(int,int)), this, SLOT(episodeAdded(int,int)));
+       connect(MyList::instance()->database(), SIGNAL(episodeDelete(int,int)), this, SLOT(episodeDeleted(int,int)));
 }
 
 void AnimeType::update(Data *data)
 {
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT 0, %1 "
-       "WHERE aid = :aid ")
-       .arg(baseQuery()));
-       q.bindValue(":aid", data->id());
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(updateQuery());
+       q.bindValue(":id", data->id());
 
        if (!q.exec())
                return;
@@ -90,6 +158,7 @@ NodeCompare AnimeType::nodeCompareFunction() const
        {
                const AnimeData *aa = static_cast<AnimeData *>(a->data());
                const AnimeData *ab = static_cast<AnimeData *>(b->data());
+               qDebug() << "CMP" << aa->animeData.titleRomaji << ab->animeData.titleRomaji << (aa->animeData.titleRomaji < ab->animeData.titleRomaji);
                return aa->animeData.titleRomaji < ab->animeData.titleRomaji;
        };
 }
@@ -99,6 +168,11 @@ Data *AnimeType::readEntry(const SqlResultIteratorInterface &it)
        return genericReadEntry<AnimeData>(it, fillAnimeData);
 }
 
+void AnimeType::animeAdded(int aid)
+{
+       emit model()->entryAdded(this, aid);
+}
+
 void AnimeType::animeUpdated(int aid)
 {
        const auto it = m_dataStore.find(aid);
@@ -109,24 +183,85 @@ void AnimeType::animeUpdated(int aid)
        update(*it);
 }
 
+void AnimeType::animeDeleted(int aid)
+{
+       qDebug() << "animeDeleted" << aid;
+       const auto it = m_dataStore.find(aid);
+
+       if (it == m_dataStore.constEnd())
+               return;
+
+       deleted(*it);
+}
+
+void AnimeType::episodeAdded(int eid, int aid)
+{
+       // When an ep is added the ep count for anime changes
+       Q_UNUSED(eid);
+       animeUpdated(aid);
+}
+
+void AnimeType::episodeDeleted(int eid, int aid)
+{
+       Q_UNUSED(eid);
+       animeUpdated(aid);
+}
+
+void AnimeType::fileAdded(int fid, int eid, int aid)
+{
+       // When a file is added the watched ep count for anime can change
+       Q_UNUSED(fid);
+       Q_UNUSED(eid);
+       animeUpdated(aid);
+}
+
+void AnimeType::fileUpdated(int fid, int eid, int aid)
+{
+       // TODO this is not perfect because
+       // there may be a secondary anime in file_episode_rel.
+       Q_UNUSED(fid);
+       Q_UNUSED(eid);
+       animeUpdated(aid);
+}
+
+void AnimeType::fileDeleted(int fid, int eid, int aid)
+{
+       Q_UNUSED(fid);
+       Q_UNUSED(eid);
+       animeUpdated(aid);
+}
+
 void AnimeType::fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query)
 {
        data.episodesInMyList = query.value(1).toInt();
-       data.watchedEpisodes = query.value(2).toInt();
-       data.myState = query.value(3).toInt();
-       Database::readAnimeData(query, data.animeData, 4);
+       data.specialsInMyList = query.value(2).toInt();
+       data.watchedEpisodes = query.value(3).toInt();
+       data.watchedSpecials = query.value(4).toInt();
+       data.myState = query.value(5).toInt();
+       data.alternateTitles = query.value(6).toString().split(QChar('\''));
+       Database::readAnimeData(query, data.animeData, 7);
 }
 
 // =============================================================================================================
 
-QString EpisodeType::name() const
+QString EpisodeType::tableName() const
 {
        return "episode";
 }
 
-QString EpisodeType::baseQuery() const
+QString EpisodeType::alias() const
 {
-       return QString(
+       return "e";
+}
+
+QString EpisodeType::primaryKeyName() const
+{
+       return "eid";
+}
+
+QString EpisodeType::additionalColumns() const
+{
+       return QString{
        "       (SELECT MIN(my_watched) "
        "               FROM "
        "                       (SELECT my_watched "
@@ -151,29 +286,35 @@ QString EpisodeType::baseQuery() const
        "                                               FROM file f "
        "                                               JOIN file_episode_rel fer ON (fer.fid = f.fid) "
        "                                               WHERE fer.eid = e.eid) AS sq) AS sq) AS my_state, "
-       "       et.ordering, %1 "
-       "       FROM episode e "
-       "       JOIN episode_type et ON (et.type = e.type)")
-                       .arg(Database::episodeFields());
+       "       et.ordering, %1 "}
+       .arg(Database::episodeFields());
+}
+
+QString EpisodeType::orderBy() const
+{
+       return "et.ordering ASC, e.epno ASC";
 }
 
-int EpisodeType::size() const
+QString EpisodeType::additionalJoins() const
 {
-       return sizeHelper("episode", "eid");
+       return "JOIN episode_type et ON (et.type = e.type)";
 }
 
 void EpisodeType::registerd()
 {
+       connect(MyList::instance()->database(), SIGNAL(episodeInsert(int,int)), this, SLOT(episodeAdded(int,int)));
        connect(MyList::instance()->database(), SIGNAL(episodeUpdate(int,int)), this, SLOT(episodeUpdated(int,int)));
+       connect(MyList::instance()->database(), SIGNAL(episodeDelete(int,int)), this, SLOT(episodeDeleted(int,int)));
+
+       connect(MyList::instance()->database(), SIGNAL(fileInsert(int,int,int)), this, SLOT(fileAdded(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileDelete(int,int,int)), this, SLOT(fileDeleted(int,int,int)));
 }
 
 void EpisodeType::update(Data *data)
 {
-       QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
-       "SELECT 0, %1 "
-       "WHERE eid = :eid ")
-       .arg(baseQuery()));
-       q.bindValue(":eid", data->id());
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(updateQuery());
+       q.bindValue(":id", data->id());
 
        if (!q.exec())
                return;
@@ -192,7 +333,6 @@ NodeCompare EpisodeType::nodeCompareFunction() const
                if (aa->episodeTypeOrdering == ab->episodeTypeOrdering)
                        return aa->episodeData.epno < ab->episodeData.epno;
                return aa->episodeTypeOrdering < ab->episodeTypeOrdering;
-
        };
 }
 
@@ -201,6 +341,12 @@ Data *EpisodeType::readEntry(const SqlResultIteratorInterface &it)
        return genericReadEntry<EpisodeData>(it, fillEpisodeData);
 }
 
+void EpisodeType::episodeAdded(int eid, int aid)
+{
+       Q_UNUSED(aid);
+       emit model()->entryAdded(this, eid);
+}
+
 void EpisodeType::episodeUpdated(int eid, int aid)
 {
        Q_UNUSED(aid);
@@ -212,6 +358,37 @@ void EpisodeType::episodeUpdated(int eid, int aid)
        update(*it);
 }
 
+void EpisodeType::episodeDeleted(int eid, int aid)
+{
+       Q_UNUSED(aid);
+       const auto it = m_dataStore.find(eid);
+
+       if (it == m_dataStore.constEnd())
+               return;
+
+       deleted(*it);
+}
+
+void EpisodeType::fileAdded(int fid, int eid, int aid)
+{
+       Q_UNUSED(fid);
+       episodeUpdated(eid, aid);
+}
+
+void EpisodeType::fileUpdated(int fid, int eid, int aid)
+{
+       // TODO this is not perfect because
+       // there may be a secondary episode in file_episode_rel.
+       Q_UNUSED(fid);
+       episodeUpdated(eid, aid);
+}
+
+void EpisodeType::fileDeleted(int fid, int eid, int aid)
+{
+       Q_UNUSED(fid);
+       episodeUpdated(eid, aid);
+}
+
 void EpisodeType::fillEpisodeData(EpisodeData &data, const SqlResultIteratorInterface &query)
 {
        data.watchedDate = query.value(1).toDateTime();
@@ -222,47 +399,82 @@ void EpisodeType::fillEpisodeData(EpisodeData &data, const SqlResultIteratorInte
 
 // =============================================================================================================
 
-QString FileType::name() const
+QString FileType::tableName() const
 {
        return "file";
 }
 
-QString FileType::baseQuery() const
+QString FileType::alias() const
+{
+       return "f";
+}
+
+QString FileType::primaryKeyName() const
+{
+       return "fid";
+}
+
+QString FileType::additionalColumns() const
 {
        return QString(
        "%1")
                        .arg(Database::fileFields());
 }
 
-int FileType::size() const
+void FileType::registerd()
 {
-       return sizeHelper("file", "fid");
+       connect(MyList::instance()->database(), SIGNAL(fileInsert(int,int,int)), this, SLOT(fileAdded(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int)));
+       connect(MyList::instance()->database(), SIGNAL(fileDelete(int,int,int)), this, SLOT(fileDeleted(int,int,int)));
 }
 
 void FileType::update(Data *data)
 {
-       Q_UNUSED(data);
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(updateQuery());
+       q.bindValue(":id", data->id());
+
+       if (!q.exec())
+               return;
+
+       QSqlResultIterator it(q);
+
+       genericUpdate<FileData>(data, it, fillFileData);
 }
 
-void FileType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation)
+Data *FileType::readEntry(const SqlResultIteratorInterface &it)
 {
-       Q_UNUSED(parentData);
-       Q_UNUSED(oldData);
-       Q_UNUSED(newData);
-       Q_UNUSED(operation);
+       return genericReadEntry<FileData>(it, fillFileData);
 }
 
-NodeCompare FileType::nodeCompareFunction() const
+void FileType::fileAdded(int fid, int eid, int aid)
 {
-       return [](Node *a, Node *b) -> bool
-       {
-               return a < b;
-       };
+       Q_UNUSED(aid);
+       Q_UNUSED(eid);
+       emit model()->entryAdded(this, fid);
 }
 
-Data *FileType::readEntry(const SqlResultIteratorInterface &it)
+void FileType::fileUpdated(int fid, int eid, int aid)
 {
-       return genericReadEntry<FileData>(it, fillFileData);
+       Q_UNUSED(aid);
+       Q_UNUSED(eid);
+       const auto it = m_dataStore.find(fid);
+
+       if (it == m_dataStore.constEnd())
+               return;
+
+       update(*it);
+}
+
+void FileType::fileDeleted(int fid, int eid, int aid)
+{
+       Q_UNUSED(aid);
+       Q_UNUSED(eid);
+       const auto it = m_dataStore.find(fid);
+
+       if (it == m_dataStore.constEnd())
+               return;
+
+       deleted(*it);
 }
 
 void FileType::fillFileData(FileData &data, const SqlResultIteratorInterface &query)
@@ -272,44 +484,44 @@ void FileType::fillFileData(FileData &data, const SqlResultIteratorInterface &qu
 
 // =============================================================================================================
 
-QString FileLocationType::name() const
+QString FileLocationType::tableName() const
 {
        return "file_location";
 }
 
-QString FileLocationType::baseQuery() const
+QString FileLocationType::alias() const
 {
-       return QString(
-       "h.name, %1 "
-       "       FROM file_location fl "
-       "       JOIN host h ON (fl.host_id = h.host_id) ")
-                       .arg(Database::fileLocationFields());
+       return "fl";
 }
 
-int FileLocationType::size() const
+QString FileLocationType::primaryKeyName() const
 {
-       return sizeHelper("file_location", "location_id");
+       return "location_id";
 }
 
-void FileLocationType::update(Data *data)
+QString FileLocationType::additionalColumns() const
 {
-       Q_UNUSED(data);
+       return QString(
+       "h.name, %1 ")
+                       .arg(Database::fileLocationFields());
 }
 
-void FileLocationType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation)
+QString FileLocationType::additionalJoins() const
 {
-       Q_UNUSED(parentData);
-       Q_UNUSED(oldData);
-       Q_UNUSED(newData);
-       Q_UNUSED(operation);
+       return "JOIN host h ON (fl.host_id = h.host_id)";
 }
 
-NodeCompare FileLocationType::nodeCompareFunction() const
+void FileLocationType::update(Data *data)
 {
-       return [](Node *a, Node *b) -> bool
-       {
-               return a < b;
-       };
+       QSqlQuery q = MyList::instance()->database()->prepareOneShot(updateQuery());
+       q.bindValue(":id", data->id());
+
+       if (!q.exec())
+               return;
+
+       QSqlResultIterator it(q);
+
+       genericUpdate<FileLocationData>(data, it, fillFileLocationData);
 }
 
 Data *FileLocationType::readEntry(const SqlResultIteratorInterface &it)
@@ -317,51 +529,62 @@ Data *FileLocationType::readEntry(const SqlResultIteratorInterface &it)
        return genericReadEntry<FileLocationData>(it, fillFileLocationData);
 }
 
-void FileLocationType::fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query)
+void FileLocationType::fileLocationAdded(int locationId, int fid)
 {
-       data.hostName = query.value(1).toString();
-       Database::readFileLocationData(query, data.fileLocationData, 2);
+       Q_UNUSED(fid);
+       emit model()->entryAdded(this, locationId);
 }
 
-// =============================================================================================================
+void FileLocationType::fileLocationUpdated(int locationId, int fid)
+{
+       Q_UNUSED(fid);
+       const auto it = m_dataStore.find(locationId);
 
-QString AnimeTitleType::name() const
+       if (it == m_dataStore.constEnd())
+               return;
+
+       update(*it);
+}
+
+void FileLocationType::fileLocationDeleted(int locationId, int fid)
 {
-       return "anime_title";
+       Q_UNUSED(fid);
+       const auto it = m_dataStore.find(locationId);
+
+       if (it == m_dataStore.constEnd())
+               return;
+
+       deleted(*it);
 }
 
-QString AnimeTitleType::baseQuery() const
+void FileLocationType::fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query)
 {
-       return QString(
-       "%1 "
-       "       FROM anime_title at ")
-                       .arg(Database::animeTitleFields());
+       data.hostName = query.value(1).toString();
+       Database::readFileLocationData(query, data.fileLocationData, 2);
 }
 
-int AnimeTitleType::size() const
+// =============================================================================================================
+
+QString AnimeTitleType::tableName() const
 {
-       return sizeHelper("anime_title", "title");
+       return "anime_title";
 }
 
-void AnimeTitleType::update(Data *data)
+QString AnimeTitleType::alias() const
 {
-       Q_UNUSED(data);
+       return "at";
 }
 
-void AnimeTitleType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation)
+QString AnimeTitleType::primaryKeyName() const
 {
-       Q_UNUSED(parentData);
-       Q_UNUSED(oldData);
-       Q_UNUSED(newData);
-       Q_UNUSED(operation);
+       return "title_id";
 }
 
-NodeCompare AnimeTitleType::nodeCompareFunction() const
+QString AnimeTitleType::additionalColumns() const
 {
-       return [](Node *a, Node *b) -> bool
-       {
-               return a < b;
-       };
+       return QString(
+       "%1 ")
+                       .arg(Database::animeTitleFields());
 }
 
 Data *AnimeTitleType::readEntry(const SqlResultIteratorInterface &it)
index 1698970e3fb73eba7b30384c2bda413a0a5843b7..1a3c37c825d41fde96e3f4118baa41a151251aab 100644 (file)
@@ -9,44 +9,36 @@
 
 namespace LocalMyList {
 namespace DynamicModel {
-/*
-class LOCALMYLISTSHARED_EXPORT RootType : public DataType
-{
-       QString name() const;
-       QStringList availableChildRelations() const;
-
-       QString baseQuery() const;
 
-       int size() const;
-
-       void update(Data *data);
-       void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation);
+class LOCALMYLISTSHARED_EXPORT ColumnType : public DataType
+{
+       Q_OBJECT
 
-       NodeCompare nodeCompareFunction() const;
+       QString name() const override;
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const override;
 
+protected:
        Data *readEntry(const SqlResultIteratorInterface &it);
-
-private:
-       void fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query);
 };
-*/
+
 // =============================================================================================================
 
 class LOCALMYLISTSHARED_EXPORT AnimeType : public DataType
 {
        Q_OBJECT
 
-       QString name() const;
-       QStringList availableChildRelations() const;
-
-       QString baseQuery() const;
-
-       int size() const;
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const override;
+       QString orderBy() const override;
 
-       void registerd();
+       void registerd() override;
 
-       void update(Data *data);
-       void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation);
+       void update(Data *data) override;
 
        NodeCompare nodeCompareFunction() const;
 
@@ -54,7 +46,16 @@ protected:
        Data *readEntry(const SqlResultIteratorInterface &it);
 
 private slots:
+       void animeAdded(int aid);
        void animeUpdated(int aid);
+       void animeDeleted(int aid);
+
+       void episodeAdded(int eid, int aid);
+       void episodeDeleted(int eid, int aid);
+
+       void fileAdded(int fid, int eid, int aid);
+       void fileUpdated(int fid, int eid, int aid);
+       void fileDeleted(int fid, int eid, int aid);
 
 private:
        static void fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query);
@@ -66,24 +67,30 @@ class LOCALMYLISTSHARED_EXPORT EpisodeType : public DataType
 {
        Q_OBJECT
 
-       QString name() const;
-
-       QString baseQuery() const;
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const override;
+       QString orderBy() const override;
+       QString additionalJoins() const override;
 
-       int size() const;
+       void registerd() override;
 
-       void registerd();
-
-       void update(Data *data);
-       void added(int id);
+       void update(Data *data) override;
 
        NodeCompare nodeCompareFunction() const;
 
 protected:
-       Data *readEntry(const SqlResultIteratorInterface &it);
+       Data *readEntry(const SqlResultIteratorInterface &it) override;
 
 private slots:
+       void episodeAdded(int eid, int aid);
        void episodeUpdated(int eid, int aid);
+       void episodeDeleted(int eid, int aid);
+
+       void fileAdded(int fid, int eid, int aid);
+       void fileUpdated(int fid, int eid, int aid);
+       void fileDeleted(int fid, int eid, int aid);
 
 private:
        static void fillEpisodeData(EpisodeData &data, const SqlResultIteratorInterface &query);
@@ -95,16 +102,21 @@ class LOCALMYLISTSHARED_EXPORT FileType : public DataType
 {
        Q_OBJECT
 
-       QString name() const;
-       QString baseQuery() const;
-       int size() const;
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const override;
 
-       void update(Data *data);
-       void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation);
+       void registerd() override;
 
-       NodeCompare nodeCompareFunction() const;
+       void update(Data *data) override;
 
-       Data *readEntry(const SqlResultIteratorInterface &it);
+       Data *readEntry(const SqlResultIteratorInterface &it) override;
+
+private slots:
+       void fileAdded(int fid, int eid, int aid);
+       void fileUpdated(int fid, int eid, int aid);
+       void fileDeleted(int fid, int eid, int aid);
 
 private:
        static void fillFileData(FileData &data, const SqlResultIteratorInterface &query);
@@ -116,16 +128,20 @@ class LOCALMYLISTSHARED_EXPORT FileLocationType : public DataType
 {
        Q_OBJECT
 
-       QString name() const;
-       QString baseQuery() const;
-       int size() const;
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const;
+       QString additionalJoins() const override;
 
-       void update(Data *data);
-       void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation);
+       void update(Data *data) override;
 
-       NodeCompare nodeCompareFunction() const;
+       Data *readEntry(const SqlResultIteratorInterface &it) override;
 
-       Data *readEntry(const SqlResultIteratorInterface &it);
+private slots:
+       void fileLocationAdded(int locationId, int fid);
+       void fileLocationUpdated(int locationId, int fid);
+       void fileLocationDeleted(int locationId, int fid);
 
 private:
        static void fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query);
@@ -137,17 +153,12 @@ class LOCALMYLISTSHARED_EXPORT AnimeTitleType : public DataType
 {
        Q_OBJECT
 
-       QString name() const;
-       QString baseQuery() const;
-
-       int size() const;
-
-       void update(Data *data);
-       void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation);
+       QString tableName() const override;
+       QString alias() const override;
+       QString primaryKeyName() const override;
+       QString additionalColumns() const override;
 
-       NodeCompare nodeCompareFunction() const;
-
-       Data *readEntry(const SqlResultIteratorInterface &it);
+       Data *readEntry(const SqlResultIteratorInterface &it) override;
 
 private:
        static void fillAnimeTitleData(AnimeTitleData &data, const SqlResultIteratorInterface &query);
index dcb3be5ed63abfd281b5ac5945391973731fba60..b59e41bc1159e49d75621d7364f4f9958734e2d6 100644 (file)
@@ -43,7 +43,8 @@ SOURCES += \
        dynamicmodel/datatype.cpp \
        dynamicmodel/types.cpp \
        dynamicmodel/datamodel.cpp \
-       dynamicmodel/typerelation.cpp
+       dynamicmodel/typerelation.cpp \
+       dynamicmodel/queryparser.cpp
 
 HEADERS += \
        localmylist_global.h \
@@ -81,7 +82,8 @@ HEADERS += \
        dynamicmodel/dynamicmodel_global.h \
        dynamicmodel/types.h \
        dynamicmodel/datamodel.h \
-       dynamicmodel/typerelation.h
+       dynamicmodel/typerelation.h \
+       dynamicmodel/queryparser.h
 
 CONV_HEADERS += \
        include/LocalMyList/AbstractTask \
@@ -125,6 +127,8 @@ CONV_HEADERS += \
        DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT
 }
 
+INCLUDEPATH += .
+
 REV = $$system(git show-ref --head -s HEAD)
 DEFINES += REVISION=\"$${REV}\"
 
index aaf1480806dd2b37d95dc89773f33d57b0726e11..e55f5681a39c63788fd64e87db7feb4155fa1ec1 100644 (file)
@@ -376,6 +376,15 @@ CREATE OR REPLACE RULE file_location_update_notify_rule AS
                pg_notify('file_location_update', new.location_id::text || ',' || new.fid::text);
 
 -- Delete rules
+CREATE OR REPLACE RULE anime_delete_notify_rule AS
+       ON DELETE TO anime DO SELECT pg_notify('anime_delete', old.aid::text);
+
+CREATE OR REPLACE RULE episode_delete_notify_rule AS
+       ON DELETE TO episode DO SELECT pg_notify('episode_delete', old.eid::text || ',' || old.aid::text);
+
+CREATE OR REPLACE RULE file_delete_notify_rule AS
+       ON DELETE TO file DO SELECT pg_notify('file_delete', old.fid::text || ',' || old.eid::text || ',' || old.aid::text);
+
 CREATE OR REPLACE RULE file_location_delete_notify_rule AS
        ON DELETE TO file_location DO SELECT pg_notify('file_location_delete', old.location_id::text || ',' || old.fid::text);