--- /dev/null
+#include "dynamicmodelfiltermodel.h"
+
+#include "mylist.h"
+#include "settings.h"
+#include "dynamicmodel/model.h"
+#include "dynamicmodel/node.h"
+
+#include <QDebug>
+
+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<LocalMyList::DynamicModel::Model *>(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;
+}
--- /dev/null
+#ifndef DYNAMICMODELFILTERMODEL_H
+#define DYNAMICMODELFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+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
--- /dev/null
+#include "dynamicmodelitemdelegate.h"
+
+#include <QDoubleSpinBox>
+
+#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<QDoubleSpinBox *>(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<QDoubleSpinBox *>(editor);
+ model->setData(index, ed->value());
+}
+
+bool DynamicModelItemDelegate::isVoteField(const QModelIndex &index) const
+{
+ using namespace LocalMyList;
+ const DynamicModelFilterModel *model = qobject_cast<const DynamicModelFilterModel *>(index.model());
+ const DynamicModel::Node *node = model->node(index);
+
+ if (!node->data())
+ return false;
+
+ return node->data()->isVoteColumn(index.column());
+}
--- /dev/null
+#ifndef DYNAMICMODELITEMDELEGATE_H
+#define DYNAMICMODELITEMDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+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
--- /dev/null
+#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 <QHeaderView>
+#include <QMenu>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QKeyEvent>
+
+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<DynamicModelFilterModel *>(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<QAction *> 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<DynamicModel::EpisodeData *>(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<DynamicModel::FileLocationData *>(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);
+}
--- /dev/null
+#ifndef DYNAMICMODELVIEW_H
+#define DYNAMICMODELVIEW_H
+
+#include <QTreeView>
+
+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
aniaddsyntaxhighlighter.cpp \
settingsdialog.cpp \
codeeditor.cpp \
- tabs/dynamicmodeltab.cpp
+ tabs/dynamicmodeltab.cpp \
+ dynamicmodelfiltermodel.cpp \
+ dynamicmodelview.cpp \
+ dynamicmodelitemdelegate.cpp
HEADERS += mainwindow.h \
databaseconnectiondialog.h \
aniaddsyntaxhighlighter.h \
settingsdialog.h \
codeeditor.h \
- tabs/dynamicmodeltab.h
+ tabs/dynamicmodeltab.h \
+ dynamicmodelfiltermodel.h \
+ dynamicmodelview.h \
+ dynamicmodelitemdelegate.h
FORMS += mainwindow.ui \
databaseconnectiondialog.ui \
tabs/pendingrequesttab.ui \
tabs/databaselogtab.ui \
tabs/clientlogtab.ui \
- tabs/dynamicmodeltab.ui
+ tabs/dynamicmodeltab.ui
include(../localmylist.pri)
include(qtsingleapplication/qtsingleapplication.pri)
} else {
DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT
}
+
+# Why is this required with Qt5.4?
+win32 {
+ LIBS += -ladvapi32 -lshell32
+}
+
target.path = $${PREFIX}/bin
INSTALLS += target
#include "ui_dynamicmodeltab.h"
#include <QMessageBox>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QSqlError>
#include "mainwindow.h"
#include "database.h"
#include "mylist.h"
-#include "mylistfiltermodel.h"
-#include "mylistitemdelegate.h"
+#include "dynamicmodelfiltermodel.h"
+#include "dynamicmodelitemdelegate.h"
#include "dynamicmodel/model.h"
#include "dynamicmodel/datamodel.h"
#include <QDebug>
+using namespace LocalMyList;
using namespace LocalMyList::DynamicModel;
DynamicModelTab::DynamicModelTab(QWidget *parent) :
void DynamicModelTab::init()
{
+ // Model must be deleted before the DataModel is uses.
+ model = new Model(this);
+
+ // TODO: move outside the tab as it should be useful globally
dataModel = new DataModel(this);
+ dataModel->registerDataType(new ColumnType);
dataModel->registerDataType(new AnimeType);
dataModel->registerDataType(new EpisodeType);
dataModel->registerDataType(new FileType);
dataModel->registerDataType(new FileLocationType);
dataModel->registerDataType(new AnimeTitleType);
- dataModel->registerTypeRelation(new RootAnimeRelation(this));
- dataModel->registerTypeRelation(new RootEpisodeRelation(this));
- dataModel->registerTypeRelation(new AnimeEpisodeRelation(this));
- dataModel->registerTypeRelation(new EpisodeFileRelation(this));
- dataModel->registerTypeRelation(new FileFileLocationRelation(this));
- dataModel->registerTypeRelation(new RootAnimeTitleRelation(this));
- dataModel->registerTypeRelation(new AnimeTitleAnimeRelation(this));
- dataModel->registerTypeRelation(new AnimeTitleEpisodeRelation(this));
- dataModel->registerTypeRelation(new AnimeAnimeTitleRelation(this));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("anime", "episode", "aid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("episode", "anime", "aid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("anime", "file", "aid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("file", "anime", "aid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("episode", "file", "eid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("file", "episode", "eid"));
+ dataModel->registerTypeRelation(new ForeignKeyRelation("file", "file_location", "fid"));
- model = new Model(this);
- 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);
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();
}
}
+
+void DynamicModelTab::on_myListView_openFileRequested(const QModelIndex &index)
+{
+ DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+ if (!node->id())
+ return;
+
+ OpenFileData data;
+
+ if (node->data()->type()->name() == "anime")
+ {
+ data = MyList::instance()->database()->firstUnwatchedByAid(node->id());
+ }
+ else if (node->data()->type()->name() == "episode")
+ {
+ data = MyList::instance()->database()->openFileByEid(node->id());
+ }
+ else if (node->data()->type()->name() == "file")
+ {
+ data = MyList::instance()->database()->openFile(node->id());
+ }
+ else
+ {
+ return;
+ }
+
+ if (!data.fid)
+ {
+ mainWindow()->showMessage(tr("No file found."));
+ return;
+ }
+
+ QDesktopServices::openUrl(QUrl("file:///" + data.path, QUrl::TolerantMode));
+ mainWindow()->showMessage(tr("Openieng file: %1").arg(data.path));
+
+}
+
+void DynamicModelTab::on_myListView_renameFilesRequested(const QModelIndex &index)
+{
+ DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+ if (!node->id())
+ return;
+
+ QString path;
+ QSqlQuery q(MyList::instance()->database()->connection());
+
+ QChar typeLetter;
+ if (node->data()->type()->name() == "anime")
+ {
+ q.prepare(
+ "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+ " FROM file f "
+ " WHERE f.fid = fl.fid AND f.aid = :aid");
+ q.bindValue(":aid", node->id());
+
+ typeLetter = 'a';
+ }
+ else if (node->data()->type()->name() == "episode")
+ {
+ q.prepare(
+ "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+ " FROM file f "
+ " WHERE f.fid = fl.fid AND f.eid = :eid");
+ q.bindValue(":eid", node->id());
+
+ typeLetter = 'e';
+ }
+ else if (node->data()->type()->name() == "file")
+ {
+ q.prepare(
+ "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+ " WHERE fl.fid = :fid");
+ q.bindValue(":fid", node->id());
+
+ typeLetter = 'f';
+ }
+ else if (node->data()->type()->name() == "file_location")
+ {
+ q.prepare(
+ "UPDATE file_location fl SET renamed = NULL, failed_rename = false "
+ " WHERE fl.location_id = :locationId");
+ q.bindValue(":locationId", node->id());
+
+ typeLetter = 'l';
+ }
+ else
+ {
+ return;
+ }
+
+ if (!q.exec())
+ {
+ qDebug() << q.lastError();
+ return;
+ }
+
+ mainWindow()->showMessage(tr("Files for %1%2 scheduled for rename").arg(typeLetter).arg(node->id()));
+}
+
+void DynamicModelTab::on_myListView_dataRequested(const QModelIndex &index)
+{
+ DynamicModel::Node *node = dynamicModelFilterModel->node(index);
+
+ if (!node->id())
+ return;
+
+ PendingRequest r;
+
+ if (node->data()->type()->name() == "anime")
+ r.aid = node->id();
+ else if (node->data()->type()->name() == "episode")
+ r.eid = node->id();
+ else if (node->data()->type()->name() == "file")
+ r.fid = node->id();
+ else
+ return;
+
+ MyList::instance()->database()->addRequest(r);
+}
+
+void DynamicModelTab::on_myListView_removeFileLocationRequested(int id)
+{
+ Q_UNUSED(id);
+ //myListModel()->removeFileLocation(id);
+}
+
+
void DynamicModelTab::on_filterInput_textChanged(const QString &filter)
{
switch (ui->filterType->currentIndex())
{
case 1:
- myListFilterModel->setFilterWildcard(filter);
+ dynamicModelFilterModel->setFilterWildcard(filter);
break;
case 2:
- myListFilterModel->setFilterRegExp(filter);
+ dynamicModelFilterModel->setFilterRegExp(filter);
break;
case 0:
default:
- myListFilterModel->setFilterFixedString(filter);
+ dynamicModelFilterModel->setFilterFixedString(filter);
break;
}
}
void DynamicModelTab::on_filterInput_keyUpPressed()
{
- selectedRow = qMax(-1, selectedRow - 1);
- updateSelection();
+ const int rowCount{ui->myListView->model()->rowCount()};
-}
+ if (!rowCount)
+ return;
-void DynamicModelTab::on_filterInput_keyDownPressed()
-{
- int newSelectedRow = qMin(model->rowCount() - 1, selectedRow + 1);
+ const QModelIndex currentIdx{ui->myListView->selectionModel()->currentIndex()};
+ QModelIndex nextIdx{ui->myListView->model()->index(currentIdx.row() - 1, 0)};
- if (selectedRow == newSelectedRow)
- return;
+ if (!nextIdx.isValid())
+ nextIdx = ui->myListView->model()->index(rowCount - 1, 0);
- selectedRow = newSelectedRow;
- updateSelection();
+ ui->myListView->selectionModel()->
+ setCurrentIndex(nextIdx, QItemSelectionModel::ClearAndSelect
+ | QItemSelectionModel::Rows);
}
-void DynamicModelTab::on_filterInput_returnPressed()
+void DynamicModelTab::on_filterInput_keyDownPressed()
{
- if (selectedRow < 0)
+ if (!ui->myListView->model()->rowCount())
return;
- const QModelIndex idx = myListFilterModel->index(selectedRow, 0);
-// on_myListView_openFileRequested(idx);
-}
+ const QModelIndex currentIdx{ui->myListView->selectionModel()->currentIndex()};
+ QModelIndex nextIdx{ui->myListView->model()->index(currentIdx.row() + 1, 0)};
-void DynamicModelTab::currentSelectionChanged(const QModelIndex ¤t, const QModelIndex &)
-{
- selectedRow = current.row();
-}
+ if (!nextIdx.isValid())
+ nextIdx = ui->myListView->model()->index(0, 0);
-void DynamicModelTab::currentSelectionChanged()
-{
- selectedRow = -1;
+ ui->myListView->selectionModel()->
+ setCurrentIndex(nextIdx, QItemSelectionModel::ClearAndSelect
+ | QItemSelectionModel::Rows);
}
-void DynamicModelTab::updateSelection()
+void DynamicModelTab::on_filterInput_returnPressed()
{
- if (selectedRow < 0)
- {
- ui->myListView->selectionModel()->clear();
+ const QModelIndex idx{ui->myListView->selectionModel()->currentIndex()};
+
+ if (!idx.isValid())
return;
- }
- const QModelIndex idx = myListFilterModel->index(selectedRow, 0);
- ui->myListView->selectionModel()->
- setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect
- | QItemSelectionModel::Rows);
+ on_myListView_openFileRequested(idx);
}
void DynamicModelTab::on_modelQuery_returnPressed()
{
- Query q(dataModel);
+ QueryParser q(dataModel);
if (q.parse(ui->modelQuery->text()))
{
model->setQuery(q);
#include "abstracttab.h"
#include <QModelIndex>
-class MyListFilterModel;
+class DynamicModelFilterModel;
namespace LocalMyList {
namespace DynamicModel {
void changeEvent(QEvent *e);
private slots:
+ void on_myListView_openFileRequested(const QModelIndex &index);
+ void on_myListView_renameFilesRequested(const QModelIndex &index);
+ void on_myListView_dataRequested(const QModelIndex &index);
+ void on_myListView_removeFileLocationRequested(int id);
+
void on_filterInput_textChanged(const QString &filter);
void on_filterType_currentIndexChanged(int);
void on_filterInput_keyDownPressed();
void on_filterInput_returnPressed();
- void currentSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous);
- void currentSelectionChanged();
-
void on_modelQuery_returnPressed();
void on_modelQueryButton_clicked();
private:
- void updateSelection();
-
Ui::DynamicModelTab *ui;
- MyListFilterModel *myListFilterModel;
+ DynamicModelFilterModel *dynamicModelFilterModel;
LocalMyList::DynamicModel::DataModel *dataModel;
LocalMyList::DynamicModel::Model *model;
-
- int selectedRow;
};
#endif // DYNAMICMODELTAB_H
<property name="rightMargin">
<number>0</number>
</property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
</layout>
</item>
<item>
- <widget class="MyListView" name="myListView"/>
+ <widget class="DynamicModelView" name="myListView"/>
</item>
</layout>
</widget>
<header>filterlineedit.h</header>
</customwidget>
<customwidget>
- <class>MyListView</class>
+ <class>DynamicModelView</class>
<extends>QTreeView</extends>
- <header>mylistview.h</header>
+ <header>dynamicmodelview.h</header>
</customwidget>
</customwidgets>
<resources/>
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;
};
-#include "data.h"
+#include "dynamicmodel/data.h"
-#include "node.h"
-#include "datatype.h"
+#include "dynamicmodel/node.h"
+#include "dynamicmodel/datatype.h"
+#include "mylist.h"
#include <QDebug>
Q_ASSERT(references.isEmpty());
}
-QVariant Data::data(int row, int role) const
+QVariant Data::primaryValue() const
{
- Q_UNUSED(row);
+ return id();
+}
+
+QVariant Data::data(int column, int role) const
+{
+ Q_UNUSED(column);
Q_UNUSED(role);
return QVariant();
}
+bool Data::setData(int column, const QVariant &data, int role)
+{
+ Q_UNUSED(column);
+ Q_UNUSED(data);
+ Q_UNUSED(role);
+ return false;
+}
+
+Qt::ItemFlags Data::flags(int column) const
+{
+ Q_UNUSED(column);
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+bool Data::matchesFilter(const QRegExp &filter) const
+{
+ return data(0, Qt::DisplayRole).toString().contains(filter);
+}
+
+bool Data::isVoteColumn(int column) const
+{
+ Q_UNUSED(column);
+ return false;
+}
+
void Data::ref(Node *node)
{
Q_ASSERT(!references.contains(node));
void Data::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)
return animeData.aid;
}
+Qt::ItemFlags AnimeData::flags(int column) const
+{
+ Qt::ItemFlags flags = Data::flags(column);
+ if (column == 3)
+ flags |= Qt::ItemIsEditable;
+ return flags;
+}
+
+
QVariant AnimeData::data(int column, int role) const
{
+ static const QString epCountString{"%1%3 of %2%4"};
+ static const QString unknownEpCountString{"%1%3 of (%2%4)"};
+ static const QString specialsCountString{"+%1"};
switch (role)
{
case Qt::DisplayRole:
case 0:
return animeData.titleRomaji;
case 1:
- if (animeData.totalEpisodeCount)
- return QString("%1 of %2")
- .arg(episodesInMyList).arg(animeData.totalEpisodeCount);
- return QString("%1 of (%2)")
+ return (animeData.totalEpisodeCount ? epCountString : unknownEpCountString)
.arg(episodesInMyList)
- .arg(qMax(animeData.highestEpno,
- episodesInMyList));
+ .arg(animeData.totalEpisodeCount
+ ? animeData.totalEpisodeCount
+ : qMax(animeData.highestEpno, episodesInMyList))
+ .arg(specialsInMyList ? specialsCountString.arg(specialsInMyList) : "")
+ .arg("");
case 2:
if (animeData.rating < 1)
return "n/a";
return "n/a";
return QString::number(animeData.myVote, 'f', 2);
case 4:
- return QString("%1 of %2").arg(watchedEpisodes)
- .arg(episodesInMyList);
+ return epCountString.arg(watchedEpisodes)
+ .arg(episodesInMyList)
+ .arg(specialsInMyList ? specialsCountString.arg(watchedSpecials) : "")
+ .arg(specialsInMyList ? specialsCountString.arg(specialsInMyList) : "");
case 5:
return stateIdToState(myState);
}
return QVariant();
}
+bool AnimeData::setData(int column, const QVariant &data, int role)
+{
+ if (role != Qt::EditRole)
+ return false;
+
+ switch (column)
+ {
+ case 3:
+ {
+ double vote = data.toDouble();
+
+ if (qFuzzyCompare(animeData.myVote, vote))
+ return false;
+
+ if (vote < 1.0 || vote > 10.0)
+ vote = 0;
+
+ animeData.myVote = vote;
+
+ MyList::instance()->voteAnime(animeData.aid, vote);
+
+ return true;
+ }
+ }
+ return false;
+}
+
+bool AnimeData::matchesFilter(const QRegExp &filter) const
+{
+ if (Data::matchesFilter(filter))
+ return true;
+
+ for (auto &&title : alternateTitles)
+ {
+ if (title.contains(filter))
+ return true;
+ }
+ return false;
+}
+
+bool AnimeData::isVoteColumn(int column) const
+{
+ return column == 3;
+}
+
// ==========================================================
EpisodeData::EpisodeData(DataType *dataType) : Data(dataType)
return QVariant();
}
+bool EpisodeData::isVoteColumn(int column) const
+{
+ return column == 3;
+}
+
FileData::FileData(DataType *dataType) : Data(dataType)
{
}
if (!fileLocationData.renamed.isValid())
return QObject::tr("No");
if (fileLocationData.failedRename)
- return QObject::tr("Rename failed");
+ return QObject::tr("Rename failed: %1").arg(fileLocationData.renameError);
return QObject::tr("Yes, on %1").arg(fileLocationData.renamed.toString());
}
return QVariant();
virtual ~Data();
virtual int id() const = 0;
- virtual QVariant data(int row, int role) const;
+ virtual QVariant primaryValue() const;
+ virtual Qt::ItemFlags flags(int column) const;
+ virtual QVariant data(int column, int role) const;
+ virtual bool setData(int column, const QVariant &data, int role);
+ virtual bool matchesFilter(const QRegExp &filter) const;
+
+ virtual bool isVoteColumn(int column) const;
DataType *type() const { return m_type; }
// Referencing
void deref(Node *node);
void updated(Data *oldData);
- void added(Data *newData);
private:
NodeList references;
DataType * const m_type;
};
+class LOCALMYLISTSHARED_EXPORT ColumnData : public Data
+{
+public:
+ ColumnData(DataType *dataType);
+ ColumnData &operator=(ColumnData &other);
+
+ int id() const override;
+ QVariant primaryValue() const override;
+ QVariant data(int column, int role) const override;
+
+ QVariant value;
+};
+
class LOCALMYLISTSHARED_EXPORT AnimeData : public Data
{
public:
AnimeData(DataType *dataType);
AnimeData &operator=(AnimeData &other);
- int id() const;
- QVariant data(int column, int role) const;
+ int id() const override;
+ virtual Qt::ItemFlags flags(int column) const override;
+ QVariant data(int column, int role) const override;
+ bool setData(int column, const QVariant &data, int role) override;
+ bool matchesFilter(const QRegExp &filter) const override;
+
+ bool isVoteColumn(int column) const override;
Anime animeData;
int episodesInMyList;
+ int specialsInMyList;
int watchedEpisodes;
+ int watchedSpecials;
int myState;
+ QList<QString> alternateTitles;
};
class LOCALMYLISTSHARED_EXPORT EpisodeData : public Data
EpisodeData(DataType *dataType);
EpisodeData &operator=(EpisodeData &other);
- int id() const;
- QVariant data(int column, int role) const;
+ int id() const override;
+ QVariant data(int column, int role) const override;
+
+ bool isVoteColumn(int column) const override;
Episode episodeData;
QDateTime watchedDate;
FileData(DataType *dataType);
FileData &operator=(FileData &other);
- int id() const;
- QVariant data(int column, int role) const;
+ int id() const override;
+ QVariant data(int column, int role) const override;
File fileData;
};
FileLocationData(DataType *dataType);
FileLocationData &operator=(FileLocationData &other);
- int id() const;
- QVariant data(int column, int role) const;
+ int id() const override;
+ QVariant data(int column, int role) const override;
FileLocation fileLocationData;
QString hostName;
AnimeTitleData(DataType *dataType);
AnimeTitleData &operator=(AnimeTitleData &other);
- int id() const;
- QVariant data(int column, int role) const;
+ int id() const override;
+ QVariant data(int column, int role) const override;
AnimeTitle animeTitleData;
};
-#include "datamodel.h"
+#include "dynamicmodel/datamodel.h"
-#include "datatype.h"
-#include "typerelation.h"
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/typerelation.h"
+
+#include <QDebug>
namespace LocalMyList {
namespace DynamicModel {
DataModel::~DataModel()
{
+ for (auto &&relations : typeRelations)
+ qDeleteAll(relations);
qDeleteAll(dataTypes);
+ qDebug() << "Deleted data model";
}
bool DataModel::registerDataType(DataType *dataType)
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();
}
bool hasTypeRelation(const QString &source, const QString &destiantion) const;
+signals:
+ void entryAdded(DataType *dataType, int id);
private slots:
/* void animeUpdate(int aid);
#include <QtGlobal>
#include <QSqlQuery>
-#include "../database.h"
-#include "../mylist.h"
+#include "database.h"
+#include "mylist.h"
+
+#include <QDebug>
namespace LocalMyList {
namespace DynamicModel {
return m_model;
}
-QStringList DataType::availableChildRelations() const
+QString DataType::name() const
+{
+ return tableName();
+}
+
+QString DataType::orderBy() const
+{
+ return {};
+}
+
+QString DataType::additionalJoins() const
{
- return QStringList();
+ return {};
}
Data *DataType::data(int key) const
{
}
+NodeList DataType::readEntries(SqlResultIteratorInterface &it, Node *parent)
+{
+ qDebug() << "readEntries" << tableName();
+ NodeList ret;
+ while (it.next())
+ {
+ int totalRowCount = it.value(0).toInt();
+ Data *data = readEntry(it);
+ if (data->id())
+ {
+ auto it = m_dataStore.find(data->id());
+ if (it != m_dataStore.end())
+ data = it.value();
+ else
+ m_dataStore.insert(data->id(), data);
+ }
+ Node *node = new Node(parent->model(), parent, totalRowCount, data);
+ ret << node;
+ }
+ return ret;
+}
+
+QString DataType::updateQuery() const
+{
+ return QString{R"(
+ SELECT 0, %1
+ FROM %2 %3
+ %5
+ WHERE %3.%4 = :id
+ )"}
+ .arg(additionalColumns())
+ .arg(tableName())
+ .arg(alias())
+ .arg(primaryKeyName())
+ .arg(additionalJoins());
+}
+
void DataType::update(Data *data)
{
Q_UNUSED(data);
}
-void DataType::childUpdate(Node *parent, const Data *oldData, Operation operation)
+void DataType::childUpdated(Node *child, const Data * const oldData)
{
- Q_UNUSED(parent);
+ Q_UNUSED(child);
Q_UNUSED(oldData);
- Q_UNUSED(operation);
}
void DataType::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<QString> DataType::availableActions() const
+{
+ return {};
+}
+
+void DataType::actionRequested(int)
+{
+}
+
int DataType::sizeHelper(const QString &tableName, const QString &keyName) const
{
if (m_size)
DataModel *model() const;
- virtual QString name() const = 0;
- QStringList availableChildRelations() const;
-
- virtual QString baseQuery() const = 0;
+ /**
+ * @brief The name of the data type
+ * @return table name
+ */
+ virtual QString name() const;
+
+ /**
+ * @brief The name of the table this data type represents
+ * @return table name
+ */
+ virtual QString tableName() const = 0;
+
+ /**
+ * @brief The alias alias to the table returned by name()
+ * @return table alias
+ */
+ virtual QString alias() const = 0;
+
+ /**
+ * @brief The name of the primary key column in the table returned by name()
+ * @return name of the primary key
+ */
+ virtual QString primaryKeyName() const = 0;
+
+ /**
+ * @brief comma separated list of columns prefixed with the table alias that
+ * this data type requires.
+ * @return columns
+ */
+ virtual QString additionalColumns() const = 0;
+
+ /**
+ * @brief SQL ORDER BY clause
+ * @return SQL ORDER BY clause or empty string if ordering is not required
+ */
+ virtual QString orderBy() const;
+
+ /**
+ * @brief Additional joins for columns this data type requires.
+ * @return join statements or empty string
+ */
+ virtual QString additionalJoins() const;
Data *data(int key) const;
- virtual int size() const = 0;
// Register
virtual void registerd();
virtual void unregistered();
+ // Obtain
+ virtual NodeList readEntries(SqlResultIteratorInterface &it, Node *parent);
+
// Update
+ virtual QString updateQuery() const;
+
virtual void update(Data *data);
- virtual void childUpdate(Node *child, const Data *oldData, Operation operation);
+ virtual void childUpdated(Node *child, const Data *const oldData);
// Release
void released(Data *data);
// Type relation interface
virtual Data *readEntry(const SqlResultIteratorInterface &it) = 0;
+ // Actions
+ virtual QList<QString> availableActions() const;
+ virtual void actionRequested(int action);
+
protected:
int sizeHelper(const QString &tableName, const QString &keyName) const;
func(*typedData, it);
Data *newData = typedData;
- Data *currentData = data(typedData->id());
- if (currentData)
- {
- delete typedData;
- newData = currentData;
- }
- else
- {
- m_dataStore.insert(typedData->id(), newData);
- }
return newData;
}
--- /dev/null
+#include "entry.h"
+
+Entry::Entry()
+{
+}
--- /dev/null
+#ifndef ENTRY_H
+#define ENTRY_H
+
+#include <QString>
+
+class Entry
+{
+public:
+ Entry();
+
+ virtual QString fields() const = 0;
+};
+
+#endif // ENTRY_H
-#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 <QDebug>
namespace LocalMyList {
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;
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
if (!index.isValid())
return 0;
- return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ Node *node = static_cast<Node *>(index.internalPointer());
+ if (!node->data())
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+
+ return node->data()->flags(index.column());
}
QVariant Model::data(const QModelIndex &index, int role) const
return item->data(index.column(), role);
}
+bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (!index.isValid())
+ return false;
+
+ Node *item = static_cast<Node *>(index.internalPointer());
+
+ bool ret = item->setData(index.column(), value, role);
+
+ if (ret)
+ emit dataChanged(index, index);
+
+ return ret;
+}
+
QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return 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()
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
#include "../localmylist_global.h"
#include "query.h"
+#include "dynamicmodel/queryparser.h"
#include <QAbstractItemModel>
#include <QStringList>
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;
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
-#include "node.h"
-#include "datatype.h"
-
-#include "dynamicmodel_global.h"
-#include "data.h"
-#include "model.h"
-#include "typerelation.h"
+#include "dynamicmodel/node.h"
+
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/dynamicmodel_global.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/model.h"
+#include "dynamicmodel/typerelation.h"
+#include "dynamicmodel/queryparser.h"
+#include "mylist.h"
#include <QModelIndex>
#include <QDebug>
Node::Node(Model *model, Node *parent, int totalRowCount, Data *data)
: m_parent(parent), m_model(model), m_totalRowCount(totalRowCount),
- m_data(data), m_childType(0)
+ m_data(data)
{
Q_ASSERT_X((parent && data) || (!parent && !data), "dynamic model", "Root node has no data and no parent. Other nodes must have both");
Node::~Node()
{
- if (!m_data)
- return;
+ if (m_data)
+ m_data->deref(this);
- m_data->deref(this);
qDeleteAll(m_children);
}
DataType *Node::childDataType() const
{
- return m_childType;
-}
-
-void Node::setChildDataType(DataType *dataType)
-{
-// Q_ASSERT_X(dataType, "dynamicmodel", "NULL data type");
- m_childType = dataType;
+ return 0;
}
Node *Node::parent() const
int Node::columnCount() const
{
- return 5;
+ return 6;
}
int Node::row() const
bool Node::hasChildren() const
{
- if (this == m_model->rootItem)
- return true;
+// if (isRoot())
+// return true;
return totalRowCount() > 0;
}
+Model *Node::model() const
+{
+ return m_model;
+}
+
QVariant Node::data(int column, int role) const
{
// qDebug() << parent() << column;
return QObject::tr("Vote");
case 4:
return QObject::tr("Watched / Renamed");
+ case 5:
+ return QObject::tr("State");
}
return QVariant();
return m_data;
}
+bool Node::setData(int column, const QVariant &data, int role)
+{
+ if (!m_data)
+ return false;
+ return m_data->setData(column, data, role);
+}
+
int Node::totalRowCount() const
{
return m_totalRowCount;// ? m_totalRowCount : childDataType() ? childDataType()->size() : 0;
void Node::fetchMore()
{
- if (!m_childType)
- return;
qDebug() << "fetchMore" << this;
- NodeList newItems;
- TypeRelation *rel = 0;
- if (isRoot())
- rel = m_model->dataModel()->typeRelation(QString(), childDataType()->name());
- else
- rel = m_model->dataModel()->typeRelation(m_data->type()->name(), childDataType()->name());
+ DataType *dataType = model()->childDataType(level());
- if (!rel)
- return;
-
- DataType *grandChildDataType = m_model->grandChildDataType(this);
/* qDebug() << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";
qDebug() << "currentType\t" << (m_data ? m_data->type()->name() : "<root>");
qDebug() << "grandChildDataType\t" << (grandChildDataType ? grandChildDataType->name() : QString("0"));
// qDebug() << "rowCountType\t" << (rowCountType ? rowCountType->name() : QString("0"));
qDebug() << "getting from rel" << rel->sourceType() << rel->destinationType();
*/
- auto factory = childNodeFactory();
+ QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+ m_model->query().buildSql(level()));
+ bindValues(q);
+ qDebug() << LIMIT << childCount();
+ q.bindValue(":limit", LIMIT);
+ q.bindValue(":offset", childCount());
- newItems = rel->getChildren(m_data, childCount(), grandChildDataType, factory);
+ if (!MyList::instance()->database()->exec(q))
+ return;
+
+ QSqlResultIterator it(q);
+ NodeList newItems = dataType->readEntries(it, this);
+ q.finish();
const QModelIndex parent = m_model->index(this);
const int newrows = newItems.count();
}
+Node *Node::findParentOfNewEntry(const QVariantList &primaryValues)
+{
+ // Not loaded
+ if (!m_children.size() && m_totalRowCount)
+ return nullptr;
+qDebug() << level() << "magic level" << primaryValues.size();
+ if (level() == primaryValues.size())
+ return this;
+
+ const QVariant &primaryValue = primaryValues.at(level());
+
+ Node *child = findChildByPrimaryValue(primaryValue);
+
+ if (!child)
+ return nullptr;
+
+ Q_ASSERT_X(child->parent() == this, "dynamicmodel/update", "Found node that is not child of it's parent");
+qDebug() << child->level() << "magic level";
+
+ return child->findParentOfNewEntry(primaryValues);
+}
+
MoveType Node::moveChild(Node *child, Operation type)
{
qDebug() << "a";
return SuccessfulMove;
}
-int Node::depth() const
+int Node::level() const
{
Node *node = parent();
int depth = 0;
return depth;
}
+Node *Node::findChildByPrimaryValue(const QVariant &primaryValue)
+{
+ // TODO this can be made logarithmic
+ // children of type column are ordered ascending by their valie
+ // datatypes with ids can bee looked up in the DataType's dataStore,
+ // followed by node lookup.
+ for (Node *child : m_children)
+ {
+ if (child->data()->primaryValue() == primaryValue)
+ return child;
+ }
+ return 0;
+}
+
NodeFactory Node::childNodeFactory()
{
return [=](Data *d, int c) -> Node *
{
Node *n = new Node(m_model, this, c, d);
- n->setChildDataType(m_model->grandChildDataType(this));
+// n->setChildDataType(m_model->grandChildDataType(this));
return n;
};
}
+void Node::bindValues(QSqlQuery &query) const
+{
+ if (parent())
+ parent()->bindValues(query);
+ m_model->query().bindValue(query, m_data, level() - 1);
+}
+
int Node::id() const
{
return m_data->id();
}
-void Node::childAdded(Data *newData)
+bool Node::childAdded(int id, DataType *dataType)
{
- qDebug() << "childAdded" << newData;
+ qDebug() << "childAdded" << id << dataType;
-/* Node *childNode = childNodeFactory()(newData);
+ // The total row count increases regardless if we actually add a child or not
+ // as there is a row in the db that is a child of this parent
+ ++m_totalRowCount;
- MoveType moveType = moveChild(childNode, InsertOperation);
+ QSqlQuery q = MyList::instance()->database()->prepareOneShot(
+ m_model->query().buildEntrySql(level()));
+ bindValues(q);
+ q.bindValue(":id", id);
- if (moveType == OutOfBoundsMove)
- delete childNode;
-*/
-}
-/*
-void Node::childUpdate(Node *child, const Data *newData, Operation operation)
-{
+ if (!q.exec())
+ return false;
+
+ Node *child = nullptr;
+
+ {
+ QSqlResultIterator it(q);
+ NodeList nodelist = dataType->readEntries(it, this);
+ q.finish();
+ if (!nodelist.length())
+ return false;
+ child = nodelist.at(0);
+ }
+ // TODO pg orders differently than the usual node compare leading to arbitrary ordering
+ auto it = std::upper_bound(m_children.begin(), m_children.end(), child, dataType->nodeCompareFunction());
+ if (it == m_children.end())
+ return false;
+ const int newRow = qMax(0, (it - m_children.begin()) - 1);
+ m_model->beginInsertRows(m_model->index(this), newRow, newRow);
+ it = m_children.insert(it, child);
+ m_model->endInsertRows();
+ return true;
}
-*/
-bool Node::updated(Operation type)
-{
- Q_UNUSED(type);
+void Node::updated(const Data * const oldData)
+{
const int r = row();
const QModelIndex parentIndex(m_model->index(parent()));
emit m_model->dataChanged(m_model->index(r, 0, parentIndex),
m_model->index(r, columnCount() - 1, parentIndex));
- return false;
+ if (m_parent && m_parent->data())
+ m_parent->data()->type()->childUpdated(this, oldData);
+
}
} // namespace DynamicModel
#include <QVariant>
#include <QList>
+class QSqlQuery;
+
namespace LocalMyList {
namespace DynamicModel {
~Node();
DataType *childDataType() const;
- void setChildDataType(DataType *dataType);
bool isRoot() const { return !m_parent; }
int columnCount() const;
int row() const;
bool hasChildren() const;
+ Model *model() const;
// Data
int id() const;
QVariant data(int column, int role) const;
Data *data() const;
+ bool setData(int column, const QVariant &data, int role);
int totalRowCount() const;
bool canFetchMore() const;
void fetchComplete();
// Changes
- void childAdded(Data *newData);
- bool updated(Operation type);
+ Node *findParentOfNewEntry(const QVariantList &primaryValues);
+ bool childAdded(int id, DataType *dataType);
+ void updated(const Data * const oldData);
MoveType moveChild(Node *child, Operation type);
// Misc
- int depth() const;
+ int level() const;
+
+ Node *findChildByPrimaryValue(const QVariant &primaryValue);
private:
NodeFactory childNodeFactory();
+ void bindValues(QSqlQuery &query) const;
Node *m_parent;
Model *m_model;
int m_totalRowCount;
Data *m_data;
- DataType *m_childType;
+
+ static const int LIMIT = 400;
};
} // namespace DynamicModel
-#include "query.h"
+#include "dynamicmodel/query.h"
-#include "datamodel.h"
+#include "dynamicmodel/datamodel.h"
namespace LocalMyList {
namespace DynamicModel {
--- /dev/null
+#include "dynamicmodel/queryparser.h"
+
+#include <QStringList>
+#include <QSet>
+
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/typerelation.h"
+
+#include <QDebug>
+
+namespace LocalMyList {
+namespace DynamicModel {
+
+// TODO this data has to come from the data model
+namespace {
+const QMap<QString, QStringList> table_columns = []() {
+ QMap<QString, QStringList> 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<QString> ellipsisParts = []() {
+ QList<QString> 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<QString, QStringList> 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<QString> 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
--- /dev/null
+#ifndef QUERYPARSER_H
+#define QUERYPARSER_H
+
+#include "localmylist_global.h"
+#include <QString>
+#include <QVector>
+#include <QSqlQuery>
+#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<Level> m_levels;
+ mutable QVector<int> m_placeholderUse;
+ DataModel *m_dataModel;
+
+};
+
+
+QDebug operator<<(QDebug dbg, const QueryParser::Level &l);
+
+} // namespace DynamicModel
+} // namespace LocalMyList
+
+
+#endif // QUERYPARSER_H
#include "typerelation.h"
-#include "../mylist.h"
-#include "../database.h"
-#include "../databaseclasses.h"
-#include "node.h"
-#include "datatype.h"
-#include "data.h"
-#include "types.h"
+#include "dynamicmodel/node.h"
+#include "dynamicmodel/datatype.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/types.h"
+#include "mylist.h"
+#include "database.h"
+#include "databaseclasses.h"
#include <QDebug>
{
}
-IdList TypeRelation::getParents(int id)
-{
- Q_UNUSED(id);
- return IdList();
-}
+QString TypeRelation::joinCondition(const QString &, const QString &) const { return {}; }
+
DataType *TypeRelation::dataType() const
{
return m_dataType;
}
-QString TypeRelation::childRowCountQuery(DataType *type) const
-{
- static const QString zeroQuery("0");
-
- if (!type)
- return zeroQuery;
-
- TypeRelation *rel = dataType()->model()->typeRelation(destinationType(), type->name());
-
- qDebug() << "relation" << rel->sourceType() << rel->destinationType();
- if (!rel)
- return zeroQuery;
-
- return rel->rowCountQuery();
-}
-
// ===========================================================================
-RootAnimeRelation::RootAnimeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString RootAnimeRelation::sourceType() const
-{
- return QString();
-}
-
-QString RootAnimeRelation::destinationType() const
-{
- return "anime";
-}
-
-NodeList RootAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- Q_UNUSED(parent);
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, "
- "%1 "
- "ORDER BY title_romaji ASC "
- "LIMIT :limit "
- "OFFSET :offset ")
- .arg(dataType()->baseQuery())
- .arg(childRowCountQuery(rowCountType)));
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
- return newItems;
-}
-
-QString RootAnimeRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(aid) FROM anime)";
-}
-
-// =================================================
-
-RootAnimeTitleRelation::RootAnimeTitleRelation(QObject *parent) : TypeRelation(parent)
+ForeignKeyRelation::ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, const QString &fk, QObject *parent)
+ : TypeRelation{parent}, m_left{left}, m_right{right}, m_pk{pk}, m_fk{fk}
{
}
-QString RootAnimeTitleRelation::sourceType() const
+ForeignKeyRelation::ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, QObject *parent)
+ : TypeRelation{parent}, m_left{left}, m_right{right}, m_pk{pk}, m_fk{pk}
{
- return QString();
}
-QString RootAnimeTitleRelation::destinationType() const
+QString ForeignKeyRelation::sourceType() const
{
- return "anime_title";
+ return m_left;
}
-NodeList RootAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
+QString ForeignKeyRelation::destinationType() const
{
- Q_UNUSED(parent);
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, %1 "
- " ORDER BY title ASC "
- "LIMIT :limit "
- "OFFSET :offset ")
- .arg(dataType()->baseQuery())
- .arg(childRowCountQuery(rowCountType)));
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
-
- return newItems;
-}
-
-QString RootAnimeTitleRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(title_id) FROM anime_title)";
-}
-
-// =================================================
-
-
-RootEpisodeRelation::RootEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString RootEpisodeRelation::sourceType() const
-{
- return QString();
-}
-
-QString RootEpisodeRelation::destinationType() const
-{
- return "episode";
-}
-
-NodeList RootEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- Q_UNUSED(parent)
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT "
- " %2, "
- " %1 "
- " ORDER BY et.ordering ASC, e.epno ASC "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
-
- return newItems;
-}
-
-QString RootEpisodeRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(eid) FROM episode)";
-}
-
-// =================================================
-
-AnimeEpisodeRelation::AnimeEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeEpisodeRelation::sourceType() const
-{
- return "anime";
-}
-
-QString AnimeEpisodeRelation::destinationType() const
-{
- return "episode";
-}
-
-NodeList AnimeEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT "
- " %2, "
- " %1 "
- " WHERE e.aid = :aid "
- " ORDER BY et.ordering ASC, e.epno ASC "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":aid", parent->id());
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
-
- return newItems;
-}
-
-QString AnimeEpisodeRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(eid) FROM episode WHERE aid = a.aid)";
-}
-
-IdList AnimeEpisodeRelation::getParents(int id)
-{
- QSqlQuery &q = MyList::instance()->database()->prepare(
- "SELECT e.aid FROM episode e WHERE e.eid = :eid");
- q.bindValue(":eid", id);
-
- IdList ret;
-
- if (!q.exec())
- return ret;
-
- while (q.next())
- ret << q.value(0).toInt();
-
- q.finish();
-
- return ret;
-}
-
-// =================================================
-
-AnimeAnimeTitleRelation::AnimeAnimeTitleRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeAnimeTitleRelation::sourceType() const
-{
- return "anime";
-}
-
-QString AnimeAnimeTitleRelation::destinationType() const
-{
- return "anime_title";
-}
-
-NodeList AnimeAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT "
- " %2, "
- " %1 "
- " WHERE at.aid = :aid "
- " ORDER BY at.type ASC, at.title ASC "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":aid", parent->id());
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
-
- return newItems;
-}
-
-QString AnimeAnimeTitleRelation::rowCountQuery() const
-{
- return "(SELECT count(title_id) FROM anime_title WHERE aid = a.aid)";
-}
-
-// =================================================
-
-EpisodeFileRelation::EpisodeFileRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString EpisodeFileRelation::sourceType() const
-{
- return "episode";
-}
-
-QString EpisodeFileRelation::destinationType() const
-{
- return "file";
-}
-
-NodeList EpisodeFileRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, %1 "
- " FROM file f "
- " WHERE f.eid = :eida "
- "UNION "
- "SELECT %2, %1 FROM file f "
- " JOIN file_episode_rel fer ON (fer.fid = f.fid) "
- " WHERE fer.eid = :eidb ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":eida", parent->id());
- q.bindValue(":eidb", parent->id());
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
- return newItems;
-}
-
-QString EpisodeFileRelation::rowCountQuery() const
-{
- return
- " (SELECT COUNT(fid) "
- " FROM ( "
- " SELECT fid "
- " FROM file "
- " WHERE eid = e.eid "
- " UNION "
- " SELECT f.fid FROM file f "
- " JOIN file_episode_rel fer ON (fer.fid = f.fid) "
- " WHERE fer.eid = e.eid) sq) ";
-}
-
-// =================================================
-
-FileFileLocationRelation::FileFileLocationRelation(QObject *parent) : TypeRelation(parent)
-{
-
-}
-
-QString FileFileLocationRelation::sourceType() const
-{
- return "file";
-}
-
-QString FileFileLocationRelation::destinationType() const
-{
- return "file_location";
-}
-
-NodeList FileFileLocationRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, %1 "
- " WHERE fl.fid = :fid "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":fid", parent->id());
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
- return newItems;
-}
-
-QString FileFileLocationRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(location_id) FROM file_location WHERE fid = f.fid)";
-}
-
-// =================================================
-
-AnimeTitleAnimeRelation::AnimeTitleAnimeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeTitleAnimeRelation::sourceType() const
-{
- return "anime_title";
-}
-
-QString AnimeTitleAnimeRelation::destinationType() const
-{
- return "anime";
-}
-
-NodeList AnimeTitleAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- int aid = static_cast<AnimeTitleData *>(parent)->animeTitleData.aid;
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, %1 "
- " WHERE a.aid = :aid "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":aid", aid);
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
- return newItems;
-}
-
-QString AnimeTitleAnimeRelation::rowCountQuery() const
-{
- return "(SELECT COUNT(aid) FROM anime WHERE aid = at.aid)";
-}
-
-// =================================================
-
-AnimeTitleEpisodeRelation::AnimeTitleEpisodeRelation(QObject *parent) : TypeRelation(parent)
-{
-}
-
-QString AnimeTitleEpisodeRelation::sourceType() const
-{
- return "anime_title";
-}
-
-QString AnimeTitleEpisodeRelation::destinationType() const
-{
- return "episode";
-}
-
-NodeList AnimeTitleEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory)
-{
- if (!parent)
- return NodeList();
-
- int aid = static_cast<AnimeTitleData *>(parent)->animeTitleData.aid;
-
- QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString(
- "SELECT %2, %1 "
- " WHERE e.aid = :aid "
- " LIMIT :limit "
- " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType)));
- q.bindValue(":aid", aid);
- q.bindValue(":limit", LIMIT);
- q.bindValue(":offset", offset);
-
- if (!q.exec())
- return NodeList();
-
- NodeList newItems;
- QSqlResultIterator it(q);
- while (it.next())
- {
- int totalRowCount = it.value(0).toInt();
- Data *data = dataType()->readEntry(it);
- auto node = nodeFactory(data, totalRowCount);
- newItems << node;
- }
- return newItems;
+ return m_right;
}
-QString AnimeTitleEpisodeRelation::rowCountQuery() const
+QString ForeignKeyRelation::joinCondition(const QString &leftAlias, const QString &rightAlias) const
{
- return "(SELECT COUNT(eid) FROM episode WHERE aid = at.aid)";
+ return QString{"%2.%3 = %1.%4"}
+ .arg(rightAlias).arg(leftAlias).arg(m_pk).arg(m_fk);
}
} // namespace DynamicModel
virtual QString sourceType() const = 0;
virtual QString destinationType() const = 0;
-
- virtual NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) = 0;
- virtual QString rowCountQuery() const = 0;
- virtual IdList getParents(int id);
+ virtual QString joinCondition(const QString &leftAlias, const QString &rightAlias) const;
DataType *dataType() const;
-protected:
- QString childRowCountQuery(DataType *type) const;
-
- static const int LIMIT = 400;
-
private:
DataType *m_dataType;
};
// =========================================================================================================
-class LOCALMYLISTSHARED_EXPORT RootAnimeRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- RootAnimeRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT RootAnimeTitleRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- RootAnimeTitleRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT RootEpisodeRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- RootEpisodeRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeEpisodeRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- AnimeEpisodeRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
- IdList getParents(int id);
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeAnimeTitleRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- AnimeAnimeTitleRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT EpisodeFileRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- EpisodeFileRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-/*
-class LOCALMYLISTSHARED_EXPORT EpisodeFileLocationRelation : public TypeRelation
+class LOCALMYLISTSHARED_EXPORT ForeignKeyRelation : public TypeRelation
{
- Q_OBJECT
public:
- EpisodeFileLocationRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
+ ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, const QString &fk, QObject *parent = 0);
+ ForeignKeyRelation(const QString &left, const QString &right, const QString &pk, QObject *parent = 0);
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-*/
+ virtual QString sourceType() const override;
+ virtual QString destinationType() const override;
+ QString joinCondition(const QString &leftAlias, const QString &rightAlias) const override;
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT FileFileLocationRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- FileFileLocationRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeTitleAnimeRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- AnimeTitleAnimeRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
-};
-
-// =========================================================================================================
-
-class LOCALMYLISTSHARED_EXPORT AnimeTitleEpisodeRelation : public TypeRelation
-{
- Q_OBJECT
-public:
- AnimeTitleEpisodeRelation(QObject *parent);
-
- QString sourceType() const;
- QString destinationType() const;
-
- NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory);
- QString rowCountQuery() const;
+private:
+ QString m_left;
+ QString m_right;
+ QString m_pk;
+ QString m_fk;
};
} // namespace DynamicModel
#include "types.h"
-#include "../database.h"
-#include "../mylist.h"
-#include "datamodel.h"
-#include "typerelation.h"
-#include "data.h"
-#include "node.h"
+#include "dynamicmodel/datamodel.h"
+#include "dynamicmodel/typerelation.h"
+#include "dynamicmodel/data.h"
+#include "dynamicmodel/node.h"
+#include "database.h"
+#include "mylist.h"
#include <QDebug>
namespace LocalMyList {
namespace DynamicModel {
-QString AnimeType::name() const
+QString ColumnType::name() const
{
- return "anime";
+ return "column";
}
-QStringList AnimeType::availableChildRelations() const
+QString ColumnType::tableName() const
{
- return QStringList();
+ return {};
}
-QString AnimeType::baseQuery() const
+QString ColumnType::alias() const
{
- return QString(
- " (SELECT COUNT(e.eid) "
- " FROM episode e "
- " WHERE e.aid = a.aid), "
- " (SELECT COUNT(DISTINCT eid) "
- " FROM "
- " (SELECT e.eid FROM episode e "
- " JOIN file f ON (f.eid = e.eid) "
- " WHERE e.aid = a.aid "
- " AND f.my_watched IS NOT NULL "
- " UNION "
- " SELECT e.eid FROM episode e "
- " JOIN file_episode_rel fer ON fer.eid = e.eid "
- " JOIN file f ON f.fid = fer.fid "
- " WHERE e.aid = a.aid "
- " AND f.my_watched IS NOT NULL) sq), "
- " (SELECT CASE WHEN array_length(my_state_array, 1) > 1 THEN -1 ELSE my_state_array[1] END "
- " FROM "
- " (SELECT array_agg(my_state) my_state_array "
- " FROM "
- " (SELECT my_state "
- " FROM file "
- " WHERE aid = a.aid "
- " UNION "
- " SELECT f.my_state "
- " FROM file f "
- " JOIN file_episode_rel fer ON (fer.fid = f.eid) "
- " JOIN episode e ON (e.eid = fer.eid AND e.aid = a.aid) "
- " ) AS sq) AS sq) AS my_state, "
- " %1 "
- " FROM anime a ")
- .arg(Database::animeFields());
+ return {};
}
-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<ColumnData *>(a->data());
+ const ColumnData *ab = static_cast<ColumnData *>(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;
{
const AnimeData *aa = static_cast<AnimeData *>(a->data());
const AnimeData *ab = static_cast<AnimeData *>(b->data());
+ qDebug() << "CMP" << aa->animeData.titleRomaji << ab->animeData.titleRomaji << (aa->animeData.titleRomaji < ab->animeData.titleRomaji);
return aa->animeData.titleRomaji < ab->animeData.titleRomaji;
};
}
return genericReadEntry<AnimeData>(it, fillAnimeData);
}
+void AnimeType::animeAdded(int aid)
+{
+ Q_UNUSED(aid);
+ emit model()->entryAdded(this, aid);
+}
+
void AnimeType::animeUpdated(int aid)
{
const auto it = m_dataStore.find(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 "
" 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;
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();
// =============================================================================================================
-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<FileData>(data, it, fillFileData);
}
NodeCompare FileType::nodeCompareFunction() const
return genericReadEntry<FileData>(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);
// =============================================================================================================
-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)
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
// =============================================================================================================
-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
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;
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);
{
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);
{
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);
{
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);
{
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);
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 \
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 \
DEFINES += LOCALMYLIST_NO_ANIDBUDPCLIENT
}
+INCLUDEPATH += .
+
REV = $$system(git show-ref --head -s HEAD)
DEFINES += REVISION=\"$${REV}\"
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());
return d->indexOf(name);
}
+QString SqlAsyncQuery::fieldName(int i) const
+{
+ return d->fieldName(i);
+}
+
void SqlAsyncQuery::finish()
{
return d->finish();
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;
return result->fieldNames.indexOf(name);
}
+QString SqlAsyncQueryInternal::fieldName(int i) const
+{
+ return result->fieldNames[i];
+}
+
void SqlAsyncQueryInternal::finish()
{
if (working)
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;
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
#include <QUrl>
#include "mylist.h"
#include "settings.h"
-#include "queryparser.h"
+#include "dynamicmodel/queryparser.h"
+#include "dynamicmodel/types.h"
+#include "dynamicmodel/typerelation.h"
#include <QDebug>
-
using namespace LocalMyList;
+using namespace LocalMyList::DynamicModel;
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;
}
TEMPLATE = app
SOURCES += main.cpp \
- queryparser.cpp \
tabledata.cpp
include(../localmylist.pri)
INSTALLS += target
HEADERS += \
- queryparser.h \
tabledata.h
+++ /dev/null
-#include "queryparser.h"
-#include <QStringList>
-#include "tabledata.h"
-//#include "conversions.h"
-
-#include <QDebug>
-
-
-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;
-}
-
+++ /dev/null
-#ifndef QUERYPARSER_H
-#define QUERYPARSER_H
-
-#include <QString>
-#include <QVector>
-#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<Level> m_levels;
- DataModel *m_dataModel;
-
-};
-
-#endif // QUERYPARSER_H
const QMap<QString, QMap<QString, QString>> join_map = []() {
QMap<QString, QMap<QString, QString>> 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;
}();