--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
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 \
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 \
tabs/pendingrequesttab.ui \
tabs/databaselogtab.ui \
tabs/clientlogtab.ui \
- tabs/dynamicmodeltab.ui
+ tabs/dynamicmodeltab.ui
include(../localmylist.pri)
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
#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) :
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);
<< 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()
}
}
+
+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;
}
}
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 ¤t, 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();
}
#include "abstracttab.h"
#include <QModelIndex>
-class MyListFilterModel;
+class DynamicModelFilterModel;
namespace LocalMyList {
namespace DynamicModel {
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);
void on_filterInput_keyDownPressed();
void on_filterInput_returnPressed();
- void currentSelectionChanged(const QModelIndex ¤t, 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
<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">
</layout>
</item>
<item>
- <widget class="MyListView" name="myListView"/>
+ <widget class="DynamicModelView" name="myListView"/>
</item>
</layout>
</widget>
<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/>
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");
}
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);
/**
* @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);
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:
-#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>
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));
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())
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)
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:
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";
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);
}
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)
return QVariant();
}
+bool EpisodeData::isVoteColumn(int column) const
+{
+ return column == 3;
+}
+
FileData::FileData(DataType *dataType) : Data(dataType)
{
}
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();
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
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
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;
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;
};
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;
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;
};
-#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 {
DataModel::~DataModel()
{
+ for (auto &&relations : typeRelations)
+ qDeleteAll(relations);
qDeleteAll(dataTypes);
+ qDebug() << "Deleted data model";
}
bool DataModel::registerDataType(DataType *dataType)
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
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);
#include <QtGlobal>
#include <QSqlQuery>
-#include "../database.h"
-#include "../mylist.h"
+#include "database.h"
+#include "mylist.h"
+
+#include <QDebug>
namespace LocalMyList {
namespace DynamicModel {
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
{
}
+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)
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;
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;
}
-#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
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
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))
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()
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
#define MODEL_H
#include "../localmylist_global.h"
+#include "dynamicmodel/queryparser.h"
#include <QAbstractItemModel>
#include <QStringList>
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
-#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>
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");
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
int Node::columnCount() const
{
- return 5;
+ return 6;
}
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;
return QObject::tr("Vote");
case 4:
return QObject::tr("Watched / Renamed");
+ case 5:
+ return QObject::tr("State");
}
return QVariant();
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;
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();
}
+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";
}
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;
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
#include <QVariant>
#include <QList>
+class QSqlQuery;
+
namespace LocalMyList {
namespace DynamicModel {
~Node();
DataType *childDataType() const;
- void setChildDataType(DataType *dataType);
bool isRoot() const { return !m_parent; }
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;
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;
int m_totalRowCount;
Data *m_data;
- DataType *m_childType;
+
+ static const int LIMIT = 400;
};
} // namespace DynamicModel
--- /dev/null
+#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
--- /dev/null
+#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
#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>
{
}
-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
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
#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;
{
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;
};
}
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);
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 "
" 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;
if (aa->episodeTypeOrdering == ab->episodeTypeOrdering)
return aa->episodeData.epno < ab->episodeData.epno;
return aa->episodeTypeOrdering < ab->episodeTypeOrdering;
-
};
}
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);
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();
// =============================================================================================================
-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)
// =============================================================================================================
-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)
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)
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;
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);
{
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);
{
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);
{
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);
{
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);
dynamicmodel/datatype.cpp \
dynamicmodel/types.cpp \
dynamicmodel/datamodel.cpp \
- dynamicmodel/typerelation.cpp
+ dynamicmodel/typerelation.cpp \
+ dynamicmodel/queryparser.cpp
HEADERS += \
localmylist_global.h \
dynamicmodel/dynamicmodel_global.h \
dynamicmodel/types.h \
dynamicmodel/datamodel.h \
- dynamicmodel/typerelation.h
+ dynamicmodel/typerelation.h \
+ dynamicmodel/queryparser.h
CONV_HEADERS += \
include/LocalMyList/AbstractTask \
DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT
}
+INCLUDEPATH += .
+
REV = $$system(git show-ref --head -s HEAD)
DEFINES += REVISION=\"$${REV}\"
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);