From: APTX Date: Wed, 31 Dec 2014 23:37:32 +0000 (+0100) Subject: Progress!! X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=refs%2Fheads%2Fdynamicmodel;p=localmylist.git Progress!! (see master branch for actual commit with new dynamic model) --- diff --git a/localmylist-management/dynamicmodelfiltermodel.cpp b/localmylist-management/dynamicmodelfiltermodel.cpp new file mode 100644 index 0000000..f2ade69 --- /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..9d98d25 --- /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..9d8ab4e --- /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..cdd6624 --- /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..3ea75a0 --- /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..2ec824e --- /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..218a1af 100644 --- a/localmylist-management/localmylist-management.pro +++ b/localmylist-management/localmylist-management.pro @@ -32,7 +32,10 @@ SOURCES += main.cpp\ aniaddsyntaxhighlighter.cpp \ settingsdialog.cpp \ codeeditor.cpp \ - tabs/dynamicmodeltab.cpp + tabs/dynamicmodeltab.cpp \ + dynamicmodelfiltermodel.cpp \ + dynamicmodelview.cpp \ + dynamicmodelitemdelegate.cpp HEADERS += mainwindow.h \ databaseconnectiondialog.h \ @@ -55,7 +58,10 @@ HEADERS += mainwindow.h \ aniaddsyntaxhighlighter.h \ settingsdialog.h \ codeeditor.h \ - tabs/dynamicmodeltab.h + tabs/dynamicmodeltab.h \ + dynamicmodelfiltermodel.h \ + dynamicmodelview.h \ + dynamicmodelitemdelegate.h FORMS += mainwindow.ui \ databaseconnectiondialog.ui \ @@ -67,7 +73,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 +86,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 e016f05..33b5bb4 100644 --- a/localmylist-management/tabs/dynamicmodeltab.cpp +++ b/localmylist-management/tabs/dynamicmodeltab.cpp @@ -2,12 +2,15 @@ #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" @@ -16,6 +19,7 @@ #include +using namespace LocalMyList; using namespace LocalMyList::DynamicModel; DynamicModelTab::DynamicModelTab(QWidget *parent) : @@ -46,28 +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); - 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); @@ -87,9 +93,8 @@ void DynamicModelTab::init() 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"); - Query q(dataModel); - q.parse("anime|episode"); + QueryParser q(dataModel); + q.parse("episode.epno/anime/..."); if (!q.isValid()) { qDebug() << "Invalid query" << q.errorString(); @@ -120,19 +125,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; } } @@ -144,58 +278,51 @@ 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() { - Query q(dataModel); + QueryParser q(dataModel); if (q.parse(ui->modelQuery->text())) { model->setQuery(q); 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.h b/localmylist/database.h index de7e97a..6377008 100644 --- a/localmylist/database.h +++ b/localmylist/database.h @@ -25,6 +25,7 @@ public: QVariant value(int index) const { return q.value(index); } QVariant value(const QString &name) const { return q.record().value(name); } int indexOf(const QString &name ) const { return q.record().indexOf(name); } + QString fieldName(int i) const override {return q.record().fieldName(i); } private: QSqlQuery &q; }; diff --git a/localmylist/dynamicmodel/data.cpp b/localmylist/dynamicmodel/data.cpp index 0b06031..5b333c7 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)); @@ -64,22 +95,40 @@ 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->updated(oldData); // node->parent()->childUpdate(node, oldData, UpdateOperation); } } -void Data::added(Data *newData) +ColumnData::ColumnData(DataType *dataType) : Data{dataType} { - foreach (Node *node, references) - { - if (node->childDataType() == newData->type()) - node->childAdded(newData); - } + +} + +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 +150,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 +172,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 +188,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 +221,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 +338,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 +421,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..77a4ded 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,46 @@ public: void deref(Node *node); void updated(Data *oldData); - void added(Data *newData); 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 +84,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 +101,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 +113,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 +126,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 3dde9e8..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) @@ -91,10 +96,8 @@ TypeRelation *DataModel::typeRelation(const QString &source, const QString &dest 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(); } diff --git a/localmylist/dynamicmodel/datamodel.h b/localmylist/dynamicmodel/datamodel.h index 0e42b57..6d907d8 100644 --- a/localmylist/dynamicmodel/datamodel.h +++ b/localmylist/dynamicmodel/datamodel.h @@ -33,6 +33,8 @@ public: 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..bffbba9 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,78 @@ 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::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; } +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..cbfbf22 100644 --- a/localmylist/dynamicmodel/datatype.h +++ b/localmylist/dynamicmodel/datatype.h @@ -29,21 +29,63 @@ 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); // Release void released(Data *data); @@ -53,6 +95,10 @@ public: // 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 +109,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/entry.cpp b/localmylist/dynamicmodel/entry.cpp new file mode 100644 index 0000000..39eb94b --- /dev/null +++ b/localmylist/dynamicmodel/entry.cpp @@ -0,0 +1,5 @@ +#include "entry.h" + +Entry::Entry() +{ +} diff --git a/localmylist/dynamicmodel/entry.h b/localmylist/dynamicmodel/entry.h new file mode 100644 index 0000000..5651087 --- /dev/null +++ b/localmylist/dynamicmodel/entry.h @@ -0,0 +1,14 @@ +#ifndef ENTRY_H +#define ENTRY_H + +#include + +class Entry +{ +public: + Entry(); + + virtual QString fields() const = 0; +}; + +#endif // ENTRY_H diff --git a/localmylist/dynamicmodel/model.cpp b/localmylist/dynamicmodel/model.cpp index ffa2b9e..ad240ff 100644 --- a/localmylist/dynamicmodel/model.cpp +++ b/localmylist/dynamicmodel/model.cpp @@ -1,11 +1,12 @@ -#include "model.h" - -#include "node.h" -#include "datamodel.h" -#include "datatype.h" -#include "typerelation.h" -#include "query.h" - +#include "dynamicmodel/model.h" + +#include "dynamicmodel/node.h" +#include "dynamicmodel/datamodel.h" +#include "dynamicmodel/datatype.h" +#include "dynamicmodel/typerelation.h" +#include "dynamicmodel/query.h" +#include "dynamicmodel/queryparser.h" +#include "mylist.h" #include namespace LocalMyList { @@ -19,15 +20,17 @@ Model::Model(QObject *parent) : Model::~Model() { + qDebug() << "deleting model"; delete rootItem; + qDebug() << "deleted model"; } -Query Model::query() const +QueryParser Model::query() const { return m_query; } -void Model::setQuery(const Query &query) +void Model::setQuery(const QueryParser &query) { if (query == m_query) return; @@ -35,12 +38,20 @@ void Model::setQuery(const Query &query) 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.queryString()); + emit queryChanged(query.query()); } QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const @@ -56,7 +67,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 @@ -69,6 +84,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)) @@ -179,18 +209,11 @@ DataModel *Model::dataModel() const return m_query.dataModel(); } -DataType *Model::grandChildDataType(Node *node) const -{ - int d = node->depth() + 1; - - return childDataType(d); -} - DataType *Model::childDataType(int i) const { - if (m_query.dataTypeNames().count() <= i) + if (i > m_query.levels()) return 0; - return dataModel()->dataType(m_query.dataTypeNames().at(i)); + return m_query.dataType(i); } void Model::reload() @@ -201,56 +224,134 @@ void Model::reload() endResetModel(); } +void Model::entryAdded(DataType *dataType, int id) +{ + 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); - DataType *episodeDataType = m_query.dataModel()->dataType("episode"); + Q_UNUSED(eid); +// DataType *episodeDataType = m_query.dataModel()->dataType("episode"); - if (!episodeDataType) - return; +// if (!episodeDataType) +// return; - if (!m_query.dataModel()->dataType("anime")) - return; +// if (!m_query.dataModel()->dataType("anime")) +// return; - QString previousDataTypeName = QString(); -// DataType *previousDataType = 0; +// QString previousDataTypeName = QString(); +//// DataType *previousDataType = 0; - for (const QString &dataTypeName : m_query.dataTypeNames()) - { - DataType *currentDataType = m_query.dataModel()->dataType(dataTypeName); +// 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 (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); +// if (previousDataTypeName.isNull()) +// { +// // The root is the parent, just see if it needs to be added. +// } +// else +// { +// IdList ids = rel->getParents(eid); - } - } +// } +// } - previousDataTypeName = dataTypeName; - } +// previousDataTypeName = dataTypeName; +// } } Node *Model::createRootNode() { - int size = (m_query.dataModel() && !m_query.dataTypeNames().isEmpty()) - ? dataModel()->dataType(m_query.dataTypeNames().at(0))->size() - : 0; + int size = rootNodeSize(); Node *n = new Node(this, 0, size, 0); qDebug() << "SIZE" << size; - if (m_query.dataModel() && !m_query.dataTypeNames().isEmpty()) - n->setChildDataType(dataModel()->dataType(m_query.dataTypeNames().at(0))); return n; } +int Model::rootNodeSize() const +{ + if (!m_query.isValid()) + return 0; + + QSqlQuery q = MyList::instance()->database()->prepareOneShot( + query().buildCountSql(-1)); + + if (!MyList::instance()->database()->exec(q)) + return 0; + + if (!q.next()) + return 0; + + int count = q.value(0).toInt(); + + q.finish(); + + return count; +} + +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) + { + rootItem->childAdded(id, dataType); + return; + } + + QSqlQuery q = MyList::instance()->database()->prepareOneShot( + query().buildPrimaryValuesSql(currentLevel)); + + 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); + } + + 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(); +} + +Data *Model::entryAddedToNode(Node *node, int id, DataType *dataType, Data *data) +{ + 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 ee4cb6f..5c3ca3e 100644 --- a/localmylist/dynamicmodel/model.h +++ b/localmylist/dynamicmodel/model.h @@ -3,6 +3,7 @@ #include "../localmylist_global.h" #include "query.h" +#include "dynamicmodel/queryparser.h" #include #include @@ -17,29 +18,34 @@ class Query; class LOCALMYLISTSHARED_EXPORT Model : public QAbstractItemModel { Q_OBJECT - Q_PROPERTY(Query 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(); - Query query() const; - void setQuery(const Query &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; @@ -47,25 +53,29 @@ public: DataType *rootDataType() const; DataModel *dataModel() const; - DataType *grandChildDataType(Node *node) const; DataType *childDataType(int i) const; public slots: void reload(); private slots: + void entryAdded(DataType *dataType, int id); void episodeInsert(int aid, int eid); signals: - void queryChanged(Query query); + 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; - Query m_query; + QueryParser m_query; }; } // namespace DynamicModel diff --git a/localmylist/dynamicmodel/node.cpp b/localmylist/dynamicmodel/node.cpp index 9baa623..3463558 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()); + DataType *dataType = model()->childDataType(level()); - if (!rel) - return; - - 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"; @@ -258,7 +288,7 @@ qDebug() << "g"; return SuccessfulMove; } -int Node::depth() const +int Node::level() const { Node *node = parent(); int depth = 0; @@ -270,49 +300,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..baa672b 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,19 @@ 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); // 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 +69,8 @@ private: int m_totalRowCount; Data *m_data; - DataType *m_childType; + + static const int LIMIT = 400; }; } // namespace DynamicModel diff --git a/localmylist/dynamicmodel/query.cpp b/localmylist/dynamicmodel/query.cpp index 5a1626f..a812f68 100644 --- a/localmylist/dynamicmodel/query.cpp +++ b/localmylist/dynamicmodel/query.cpp @@ -1,6 +1,6 @@ -#include "query.h" +#include "dynamicmodel/query.h" -#include "datamodel.h" +#include "dynamicmodel/datamodel.h" namespace LocalMyList { namespace DynamicModel { diff --git a/localmylist/dynamicmodel/queryparser.cpp b/localmylist/dynamicmodel/queryparser.cpp new file mode 100644 index 0000000..4320809 --- /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..0b5db34 --- /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..c9e70c0 100644 --- a/localmylist/dynamicmodel/types.cpp +++ b/localmylist/dynamicmodel/types.cpp @@ -1,80 +1,155 @@ #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 {}; } -int AnimeType::size() const +QString ColumnType::primaryKeyName() const { - return sizeHelper("anime", "aid"); + return {}; +} + +QString ColumnType::additionalColumns() const +{ + return {}; +} + +NodeCompare ColumnType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + const ColumnData *aa = static_cast(a->data()); + const ColumnData *ab = static_cast(b->data()); + return aa->value < ab->value; + }; +} + +Data *ColumnType::readEntry(const SqlResultIteratorInterface &it) +{ + auto typedData = new ColumnData(this); + typedData->value = it.value(1); + return typedData; +} + +// ============================================================================================================= + +QString AnimeType::tableName() const +{ + 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(animeUpdate(int)), this, SLOT(animeUpdated(int))); + connect(MyList::instance()->database(), SIGNAL(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int))); + + connect(MyList::instance()->database(), SIGNAL(episodeInsert(int,int)), this, SLOT(episodeAdded(int,int))); + connect(MyList::instance()->database(), SIGNAL(fileInsert(int,int,int)), this, SLOT(fileAdded(int,int,int))); + + connect(MyList::instance()->database(), SIGNAL(animeInsert(int)), this, SLOT(animeAdded(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 +165,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 +175,12 @@ Data *AnimeType::readEntry(const SqlResultIteratorInterface &it) return genericReadEntry(it, fillAnimeData); } +void AnimeType::animeAdded(int aid) +{ + Q_UNUSED(aid); + emit model()->entryAdded(this, aid); +} + void AnimeType::animeUpdated(int aid) { const auto it = m_dataStore.find(aid); @@ -109,24 +191,61 @@ void AnimeType::animeUpdated(int aid) update(*it); } +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::episodeAdded(int eid, int aid) +{ + // When an ep is added the ep count for anime changes + 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::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 +270,32 @@ 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()); } -int EpisodeType::size() const +QString EpisodeType::orderBy() const { - return sizeHelper("episode", "eid"); + return "et.ordering ASC, e.epno ASC"; +} + +QString EpisodeType::additionalJoins() const +{ + return "JOIN episode_type et ON (et.type = e.type)"; } void EpisodeType::registerd() { connect(MyList::instance()->database(), SIGNAL(episodeUpdate(int,int)), this, SLOT(episodeUpdated(int,int))); + connect(MyList::instance()->database(), SIGNAL(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int))); + + connect(MyList::instance()->database(), SIGNAL(fileInsert(int,int,int)), this, SLOT(fileAdded(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; @@ -212,6 +334,20 @@ void EpisodeType::episodeUpdated(int eid, int aid) update(*it); } +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::fileAdded(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,34 +358,44 @@ 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(fileUpdate(int,int,int)), this, SLOT(fileUpdated(int,int,int))); } void FileType::update(Data *data) { - Q_UNUSED(data); -} + QSqlQuery q = MyList::instance()->database()->prepareOneShot(updateQuery()); + q.bindValue(":id", data->id()); -void FileType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) -{ - Q_UNUSED(parentData); - Q_UNUSED(oldData); - Q_UNUSED(newData); - Q_UNUSED(operation); + if (!q.exec()) + return; + + QSqlResultIterator it(q); + + genericUpdate(data, it, fillFileData); } NodeCompare FileType::nodeCompareFunction() const @@ -265,6 +411,18 @@ Data *FileType::readEntry(const SqlResultIteratorInterface &it) return genericReadEntry(it, fillFileData); } +void FileType::fileUpdated(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; + + update(*it); +} + void FileType::fillFileData(FileData &data, const SqlResultIteratorInterface &query) { Database::readFileData(query, data.fileData, 1); @@ -272,23 +430,31 @@ 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 "fl"; +} + +QString FileLocationType::primaryKeyName() const +{ + return "location_id"; +} + +QString FileLocationType::additionalColumns() const { return QString( - "h.name, %1 " - " FROM file_location fl " - " JOIN host h ON (fl.host_id = h.host_id) ") + "h.name, %1 ") .arg(Database::fileLocationFields()); } -int FileLocationType::size() const +QString FileLocationType::additionalJoins() const { - return sizeHelper("file_location", "location_id"); + return "JOIN host h ON (fl.host_id = h.host_id)"; } void FileLocationType::update(Data *data) @@ -296,14 +462,6 @@ void FileLocationType::update(Data *data) Q_UNUSED(data); } -void FileLocationType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) -{ - Q_UNUSED(parentData); - Q_UNUSED(oldData); - Q_UNUSED(newData); - Q_UNUSED(operation); -} - NodeCompare FileLocationType::nodeCompareFunction() const { return [](Node *a, Node *b) -> bool @@ -325,35 +483,31 @@ void FileLocationType::fillFileLocationData(FileLocationData &data, const SqlRes // ============================================================================================================= -QString AnimeTitleType::name() const +QString AnimeTitleType::tableName() const { return "anime_title"; } -QString AnimeTitleType::baseQuery() const +QString AnimeTitleType::alias() const { - return QString( - "%1 " - " FROM anime_title at ") - .arg(Database::animeTitleFields()); + return "at"; } -int AnimeTitleType::size() const +QString AnimeTitleType::primaryKeyName() const { - return sizeHelper("anime_title", "title"); + return "title_id"; } -void AnimeTitleType::update(Data *data) +QString AnimeTitleType::additionalColumns() const { - Q_UNUSED(data); + return QString( + "%1 ") + .arg(Database::animeTitleFields()); } -void AnimeTitleType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) +void AnimeTitleType::update(Data *data) { - Q_UNUSED(parentData); - Q_UNUSED(oldData); - Q_UNUSED(newData); - Q_UNUSED(operation); + Q_UNUSED(data); } NodeCompare AnimeTitleType::nodeCompareFunction() const diff --git a/localmylist/dynamicmodel/types.h b/localmylist/dynamicmodel/types.h index 1698970..47e010e 100644 --- a/localmylist/dynamicmodel/types.h +++ b/localmylist/dynamicmodel/types.h @@ -9,44 +9,38 @@ namespace LocalMyList { namespace DynamicModel { -/* -class LOCALMYLISTSHARED_EXPORT RootType : public DataType -{ - QString name() const; - QStringList availableChildRelations() const; - - QString baseQuery() const; - int size() const; +class LOCALMYLISTSHARED_EXPORT ColumnType : public DataType +{ + Q_OBJECT - void update(Data *data); - void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + QString name() const override; + QString tableName() const override; + QString alias() const override; + QString primaryKeyName() const override; + QString additionalColumns() const override; NodeCompare nodeCompareFunction() const; +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; + QString tableName() const override; + QString alias() const override; + QString primaryKeyName() const override; + QString additionalColumns() const override; + QString orderBy() const override; - int size() const; + void registerd() override; - void registerd(); - - 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 +48,12 @@ protected: Data *readEntry(const SqlResultIteratorInterface &it); private slots: + void animeAdded(int aid); + void animeUpdated(int aid); + void fileUpdated(int fid, int eid, int aid); + void episodeAdded(int eid, int aid); + void fileAdded(int fid, int eid, int aid); private: static void fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query); @@ -66,24 +65,27 @@ class LOCALMYLISTSHARED_EXPORT EpisodeType : public DataType { Q_OBJECT - QString name() 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; - QString baseQuery() const; + void registerd() override; - int size() const; - - void registerd(); - - void update(Data *data); + void update(Data *data) override; void added(int id); NodeCompare nodeCompareFunction() const; protected: - Data *readEntry(const SqlResultIteratorInterface &it); + Data *readEntry(const SqlResultIteratorInterface &it) override; private slots: void episodeUpdated(int eid, int aid); + void fileUpdated(int fid, int eid, int aid); + void fileAdded(int fid, int eid, int aid); private: static void fillEpisodeData(EpisodeData &data, const SqlResultIteratorInterface &query); @@ -95,16 +97,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; + + void update(Data *data) override; NodeCompare nodeCompareFunction() const; - Data *readEntry(const SqlResultIteratorInterface &it); + Data *readEntry(const SqlResultIteratorInterface &it) override; + +private slots: + void fileUpdated(int fid, int eid, int aid); private: static void fillFileData(FileData &data, const SqlResultIteratorInterface &query); @@ -116,16 +123,17 @@ class LOCALMYLISTSHARED_EXPORT FileLocationType : public DataType { Q_OBJECT - QString name() const; - QString baseQuery() const; - int size() const; + QString tableName() const; + QString alias() const; + 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); + Data *readEntry(const SqlResultIteratorInterface &it) override; private: static void fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query); @@ -137,17 +145,16 @@ class LOCALMYLISTSHARED_EXPORT AnimeTitleType : public DataType { Q_OBJECT - QString name() const; - QString baseQuery() const; + QString tableName() const; + QString alias() const; + QString primaryKeyName() const override; + QString additionalColumns() const; - int size() const; - - 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); + 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 e36ade5..90ad93a 100644 --- a/localmylist/localmylist.pro +++ b/localmylist/localmylist.pro @@ -44,8 +44,9 @@ SOURCES += \ dynamicmodel/types.cpp \ dynamicmodel/datamodel.cpp \ dynamicmodel/typerelation.cpp \ - dynamicmodel/query.cpp \ - dynamicmodel/entry.cpp + dynamicmodel/query.cpp \ + dynamicmodel/queryparser.cpp \ + dynamicmodel/entry.cpp HEADERS += \ localmylist_global.h \ @@ -84,8 +85,9 @@ HEADERS += \ dynamicmodel/types.h \ dynamicmodel/datamodel.h \ dynamicmodel/typerelation.h \ - dynamicmodel/query.h \ - dynamicmodel/entry.h + dynamicmodel/query.h \ + dynamicmodel/queryparser.h \ + dynamicmodel/entry.h CONV_HEADERS += \ include/LocalMyList/AbstractTask \ @@ -129,6 +131,8 @@ CONV_HEADERS += \ DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT } +INCLUDEPATH += . + REV = $$system(git show-ref --head -s HEAD) DEFINES += REVISION=\"$${REV}\" diff --git a/localmylist/mylistnode.cpp b/localmylist/mylistnode.cpp index cd57eb4..9361ad7 100644 --- a/localmylist/mylistnode.cpp +++ b/localmylist/mylistnode.cpp @@ -465,12 +465,13 @@ bool MyListAnimeNode::setData(int column, const QVariant &data, int role) void MyListAnimeNode::fetchMore() { qDebug() << "fetching some more for aid" << id(); - query->prepare(QString( - " %1 " - " WHERE e.aid = :aid " - " ORDER BY et.ordering ASC, e.epno ASC " - " LIMIT :limit " - " OFFSET :offset ").arg(MyListEpisodeNode::baseQuery())); + query->prepare(QString(R"( + %1 + WHERE e.aid = :aid + ORDER BY et.ordering ASC, e.epno ASC + LIMIT :limit + OFFSET :offset + )").arg(MyListEpisodeNode::baseQuery())); query->bindValue(":aid", id()); query->bindValue(":limit", LIMIT); query->bindValue(":offset", childCount()); diff --git a/localmylist/sqlasyncquery.cpp b/localmylist/sqlasyncquery.cpp index fdb7631..461fbfc 100644 --- a/localmylist/sqlasyncquery.cpp +++ b/localmylist/sqlasyncquery.cpp @@ -57,6 +57,11 @@ int SqlAsyncQuery::indexOf(const QString &name) const return d->indexOf(name); } +QString SqlAsyncQuery::fieldName(int i) const +{ + return d->fieldName(i); +} + void SqlAsyncQuery::finish() { return d->finish(); diff --git a/localmylist/sqlasyncquery.h b/localmylist/sqlasyncquery.h index 2ee6363..cdc3e49 100644 --- a/localmylist/sqlasyncquery.h +++ b/localmylist/sqlasyncquery.h @@ -36,6 +36,7 @@ public: QVariant value(int index) const; QVariant value(const QString &name) const; int indexOf(const QString &name ) const; + QString fieldName(int i) const override; void finish(); bool isWorking() const; diff --git a/localmylist/sqlasyncqueryinternal.cpp b/localmylist/sqlasyncqueryinternal.cpp index 2f74c6c..9016c07 100644 --- a/localmylist/sqlasyncqueryinternal.cpp +++ b/localmylist/sqlasyncqueryinternal.cpp @@ -120,6 +120,11 @@ int SqlAsyncQueryInternal::indexOf(const QString &name) const return result->fieldNames.indexOf(name); } +QString SqlAsyncQueryInternal::fieldName(int i) const +{ + return result->fieldNames[i]; +} + void SqlAsyncQueryInternal::finish() { if (working) diff --git a/localmylist/sqlasyncqueryinternal.h b/localmylist/sqlasyncqueryinternal.h index 92b67e0..58584b6 100644 --- a/localmylist/sqlasyncqueryinternal.h +++ b/localmylist/sqlasyncqueryinternal.h @@ -55,6 +55,7 @@ public: QVariant value(int index) const; QVariant value(const QString &name) const; int indexOf(const QString &name) const; + QString fieldName(int i) const; void finish(); QString executedQuery() const; diff --git a/localmylist/sqlresultiteratorinterface.h b/localmylist/sqlresultiteratorinterface.h index e9dea89..c50b3cf 100644 --- a/localmylist/sqlresultiteratorinterface.h +++ b/localmylist/sqlresultiteratorinterface.h @@ -11,7 +11,8 @@ public: virtual bool next() = 0; virtual QVariant value(int index) const = 0; virtual QVariant value(const QString &name) const = 0; - virtual int indexOf(const QString &name ) const = 0; + virtual int indexOf(const QString &name) const = 0; + virtual QString fieldName(int i) const = 0; }; } // namespace LocalMyList diff --git a/query-test/main.cpp b/query-test/main.cpp index d62dcca..f1365da 100644 --- a/query-test/main.cpp +++ b/query-test/main.cpp @@ -5,11 +5,13 @@ #include #include "mylist.h" #include "settings.h" -#include "queryparser.h" +#include "dynamicmodel/queryparser.h" +#include "dynamicmodel/types.h" +#include "dynamicmodel/typerelation.h" #include - using namespace LocalMyList; +using namespace LocalMyList::DynamicModel; int main(int argc, char *argv[]) { @@ -29,11 +31,31 @@ int main(int argc, char *argv[]) } DataModel d{}; + d.registerDataType(new DynamicModel::AnimeType); + d.registerDataType(new DynamicModel::EpisodeType); + d.registerDataType(new DynamicModel::FileType); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("anime", "episode", "aid")); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("episode", "anime", "aid")); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("anime", "file", "aid")); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("file", "anime", "aid")); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("episode", "file", "eid")); + qDebug() << d.registerTypeRelation(new ForeignKeyRelation("file", "episode", "eid")); QueryParser p{&d}; bool success = p.parse(a.arguments()[1]); qDebug() << "Success" << success; + if (!success) + { + qDebug() << p.errorString(); + return 1; + } + + for (int i = 0; i < p.levels(); ++i) { + qDebug() << "====" << p.query() << "level" << (i+1) << "(of" << p.levels() << ") ==="; + qDebug() << p.buildSql(i); + } + return !success; } diff --git a/query-test/query-test.pro b/query-test/query-test.pro index 7112bc2..a98c55f 100644 --- a/query-test/query-test.pro +++ b/query-test/query-test.pro @@ -11,7 +11,6 @@ CONFIG -= app_bundle TEMPLATE = app SOURCES += main.cpp \ - queryparser.cpp \ tabledata.cpp include(../localmylist.pri) @@ -20,5 +19,4 @@ target.path = $${PREFIX}/bin INSTALLS += target HEADERS += \ - queryparser.h \ tabledata.h diff --git a/query-test/queryparser.cpp b/query-test/queryparser.cpp deleted file mode 100644 index 4a80626..0000000 --- a/query-test/queryparser.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "queryparser.h" -#include -#include "tabledata.h" -//#include "conversions.h" - -#include - - -QueryParser::QueryParser(DataModel *dataModel) : m_dataModel{dataModel}, m_valid{false} -{ -} - -bool QueryParser::parse(const QString &rawPath) -{ - static const QString emptyString{}; - - m_errorString = QString{}; - - m_path = rawPath; - QStringList parts = m_path.split(QChar('/'), QString::SkipEmptyParts); - qDebug() << "parse " << parts; - - m_levels.clear(); - m_levels.resize(parts.length()); - - for (int i = 0; i < parts.length(); ++i) { - Level currentLevel; - - - 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 (!tables.contains(table)) { - m_errorString = QObject::tr("Table %1 does not exist.").arg(table); - m_valid = false; - return m_valid; - } else { - currentLevel.table = table; - currentLevel.type = AnimeEntry; - } - - 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; - } - } else { - currentLevel.column = column; - currentLevel.type = ColumnEntry; - } - - m_levels.push_back(currentLevel); - } - m_valid = true; - return m_valid; -} - -QString QueryParser::buildQuery(int level) -{ - if (!m_valid) return {}; - - const Level &lastLevel = level(level); - - QString joins; - - if (lastLevel.column.isEmpty()) { - return QString("SELECT %1.%2 FROM %1\n\t%3") - .arg(lastLevel.table).arg(lastLevel.column).arg(joins); - } -} - -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::path() const -{ - return m_path; -} - diff --git a/query-test/queryparser.h b/query-test/queryparser.h deleted file mode 100644 index 83adc4e..0000000 --- a/query-test/queryparser.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef QUERYPARSER_H -#define QUERYPARSER_H - -#include -#include -#include "dynamicmodel/datamodel.h" - -using namespace LocalMyList::DynamicModel; - -class QueryParser -{ -public: - enum EntryType { - ColumnEntry, - AnimeEntry, - EpisodeEntry, - FileEntry, - }; - - struct Level { - EntryType type; - QString table; - QString column; - }; - - QueryParser(DataModel *dataModel = 0); - - bool parse(const QString &rawPath); - - QString buildQuery(int level); - - bool isValid() const; - int levels() const; - const Level &level(int i) const; - - QString path() const; - -private: - bool m_valid; - QString m_path; - QString m_errorString; - QVector m_levels; - DataModel *m_dataModel; - -}; - -#endif // QUERYPARSER_H diff --git a/query-test/tabledata.cpp b/query-test/tabledata.cpp index e88aa33..5167a24 100644 --- a/query-test/tabledata.cpp +++ b/query-test/tabledata.cpp @@ -113,11 +113,11 @@ const QMap table_columns = []() { const QMap> join_map = []() { QMap> r; - r["anime"]["episode"] = "anime.aid = episode.aid"; - r["anime"]["file"] = "anime.aid = file.aid"; - r["episode"]["file"] = "episode.eid = file.eid"; - r["episode"]["anime"] = "episode.aid = anime.aid"; - r["file"]["anime"] = "file.aid = anime.aid"; - r["file"]["episode"] = "file.eid = episode.eid"; + r["anime"]["episode"] = "a.aid = e.aid"; + r["anime"]["file"] = "a.aid = f.aid"; + r["episode"]["file"] = "e.eid = f.eid"; + r["episode"]["anime"] = "e.aid = a.aid"; + r["file"]["anime"] = "f.aid = a.aid"; + r["file"]["episode"] = "f.eid = e.eid"; return r; }();