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