diff --git a/src/presentation/artifacteditormodel.cpp b/src/presentation/artifacteditormodel.cpp index 9b3727a8..493b31b3 100644 --- a/src/presentation/artifacteditormodel.cpp +++ b/src/presentation/artifacteditormodel.cpp @@ -1,455 +1,503 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "artifacteditormodel.h" #include +#include #include #include #include +#include #include #include #include #include "domain/task.h" #include "errorhandler.h" namespace Presentation { class AttachmentModel : public QAbstractListModel { Q_OBJECT public: explicit AttachmentModel(QObject *parent = nullptr) : QAbstractListModel(parent) { } void setTask(const Domain::Task::Ptr &task) { if (m_task == task) return; beginResetModel(); if (m_task) { disconnect(m_task.data(), &Domain::Task::attachmentsChanged, this, &AttachmentModel::triggerReset); } m_task = task; if (m_task) { connect(m_task.data(), &Domain::Task::attachmentsChanged, this, &AttachmentModel::triggerReset); } endResetModel(); } int rowCount(const QModelIndex &parent) const override { if (parent.isValid()) return 0; return m_task->attachments().size(); } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid()) return QVariant(); auto attachment = m_task->attachments().at(index.row()); switch (role) { case Qt::DisplayRole: return attachment.label(); case Qt::DecorationRole: return QVariant::fromValue(QIcon::fromTheme(attachment.iconName())); default: return QVariant(); } } private slots: void triggerReset() { beginResetModel(); endResetModel(); } private: Domain::Task::Ptr m_task; }; } using namespace Presentation; ArtifactEditorModel::ArtifactEditorModel(QObject *parent) : QObject(parent), m_done(false), m_recurrence(Domain::Task::NoRecurrence), m_attachmentModel(new AttachmentModel(this)), m_saveTimer(new QTimer(this)), m_saveNeeded(false), m_editingInProgress(false) { m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(autoSaveDelay()); connect(m_saveTimer, &QTimer::timeout, this, &ArtifactEditorModel::save); } ArtifactEditorModel::~ArtifactEditorModel() { save(); } Domain::Artifact::Ptr ArtifactEditorModel::artifact() const { return m_artifact; } void ArtifactEditorModel::setArtifact(const Domain::Artifact::Ptr &artifact) { if (m_artifact == artifact) return; save(); m_text = QString(); m_title = QString(); m_done = false; m_start = QDateTime(); m_due = QDateTime(); m_recurrence = Domain::Task::NoRecurrence; m_attachmentModel->setTask(Domain::Task::Ptr()); m_delegateText = QString(); if (m_artifact) disconnect(m_artifact.data(), Q_NULLPTR, this, Q_NULLPTR); m_artifact = artifact; if (m_artifact) { m_text = m_artifact->text(); m_title = m_artifact->title(); connect(m_artifact.data(), &Domain::Artifact::textChanged, this, &ArtifactEditorModel::onTextChanged); connect(m_artifact.data(), &Domain::Artifact::titleChanged, this, &ArtifactEditorModel::onTitleChanged); } if (auto task = artifact.objectCast()) { m_done = task->isDone(); m_start = task->startDate(); m_due = task->dueDate(); m_recurrence = task->recurrence(); m_attachmentModel->setTask(task); m_delegateText = task->delegate().display(); connect(task.data(), &Domain::Task::doneChanged, this, &ArtifactEditorModel::onDoneChanged); connect(task.data(), &Domain::Task::startDateChanged, this, &ArtifactEditorModel::onStartDateChanged); connect(task.data(), &Domain::Task::dueDateChanged, this, &ArtifactEditorModel::onDueDateChanged); connect(task.data(), &Domain::Task::recurrenceChanged, this, &ArtifactEditorModel::onRecurrenceChanged); connect(task.data(), &Domain::Task::delegateChanged, this, &ArtifactEditorModel::onDelegateChanged); } emit textChanged(m_text); emit titleChanged(m_title); emit doneChanged(m_done); emit startDateChanged(m_start); emit dueDateChanged(m_due); emit recurrenceChanged(m_recurrence); emit delegateTextChanged(m_delegateText); emit hasTaskPropertiesChanged(hasTaskProperties()); emit artifactChanged(m_artifact); } bool ArtifactEditorModel::hasSaveFunction() const { return bool(m_saveFunction); } void ArtifactEditorModel::setSaveFunction(const SaveFunction &function) { m_saveFunction = function; } bool ArtifactEditorModel::hasDelegateFunction() const { return bool(m_delegateFunction); } void ArtifactEditorModel::setDelegateFunction(const DelegateFunction &function) { m_delegateFunction = function; } bool ArtifactEditorModel::hasTaskProperties() const { return m_artifact.objectCast(); } QString ArtifactEditorModel::text() const { return m_text; } QString ArtifactEditorModel::title() const { return m_title; } bool ArtifactEditorModel::isDone() const { return m_done; } QDateTime ArtifactEditorModel::startDate() const { return m_start; } QDateTime ArtifactEditorModel::dueDate() const { return m_due; } Domain::Task::Recurrence ArtifactEditorModel::recurrence() const { return m_recurrence; } QAbstractItemModel *ArtifactEditorModel::attachmentModel() const { return m_attachmentModel; } QString ArtifactEditorModel::delegateText() const { return m_delegateText; } int ArtifactEditorModel::autoSaveDelay() { return 500; } bool ArtifactEditorModel::editingInProgress() const { return m_editingInProgress; } void ArtifactEditorModel::setText(const QString &text) { if (m_text == text) return; applyNewText(text); setSaveNeeded(true); } void ArtifactEditorModel::setTitle(const QString &title) { if (m_title == title) return; applyNewTitle(title); setSaveNeeded(true); } void ArtifactEditorModel::setDone(bool done) { if (m_done == done) return; applyNewDone(done); setSaveNeeded(true); } void ArtifactEditorModel::setStartDate(const QDateTime &start) { if (m_start == start) return; applyNewStartDate(start); setSaveNeeded(true); } void ArtifactEditorModel::setDueDate(const QDateTime &due) { if (m_due == due) return; applyNewDueDate(due); setSaveNeeded(true); } void ArtifactEditorModel::setRecurrence(Domain::Task::Recurrence recurrence) { if (m_recurrence == recurrence) return; applyNewRecurrence(recurrence); setSaveNeeded(true); } void ArtifactEditorModel::delegate(const QString &name, const QString &email) { auto task = m_artifact.objectCast(); Q_ASSERT(task); auto delegate = Domain::Task::Delegate(name, email); m_delegateFunction(task, delegate); } +void ArtifactEditorModel::addAttachment(const QString &fileName) +{ + auto task = m_artifact.objectCast(); + if (!task) + return; + + QMimeDatabase mimeDb; + auto mimeType = mimeDb.mimeTypeForFile(fileName); + + auto attachment = Domain::Task::Attachment(); + attachment.setLabel(QFileInfo(fileName).fileName()); + attachment.setMimeType(mimeType.name()); + attachment.setIconName(mimeType.iconName()); + + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) { + // TODO: Might be worth extending error handling + // to deal with job-less errors later on + qWarning() << "Couldn't open" << fileName; + return; + } + + attachment.setData(file.readAll()); + + file.close(); + + auto attachments = task->attachments(); + attachments.append(attachment); + task->setAttachments(attachments); + + setSaveNeeded(true); +} + +void ArtifactEditorModel::removeAttachment(const QModelIndex &index) +{ + auto task = m_artifact.objectCast(); + if (!task) + return; + + auto attachments = task->attachments(); + attachments.removeAt(index.row()); + task->setAttachments(attachments); + + setSaveNeeded(true); +} + void ArtifactEditorModel::openAttachment(const QModelIndex &index) { auto task = m_artifact.objectCast(); Q_ASSERT(task); auto attachment = task->attachments().at(index.row()); auto uri = attachment.uri(); if (!attachment.isUri()) { auto tempFile = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/zanshin_attachment_XXXXXX"), this); tempFile->open(); tempFile->setPermissions(QFile::ReadUser); tempFile->write(attachment.data()); tempFile->close(); uri = QUrl::fromLocalFile(tempFile->fileName()); } QDesktopServices::openUrl(uri); } void ArtifactEditorModel::setEditingInProgress(bool editing) { m_editingInProgress = editing; } void ArtifactEditorModel::onTextChanged(const QString &text) { if (!m_editingInProgress) applyNewText(text); } void ArtifactEditorModel::onTitleChanged(const QString &title) { if (!m_editingInProgress) applyNewTitle(title); } void ArtifactEditorModel::onDoneChanged(bool done) { if (!m_editingInProgress) applyNewDone(done); } void ArtifactEditorModel::onStartDateChanged(const QDateTime &start) { if (!m_editingInProgress) applyNewStartDate(start); } void ArtifactEditorModel::onDueDateChanged(const QDateTime &due) { if (!m_editingInProgress) applyNewDueDate(due); } void ArtifactEditorModel::onRecurrenceChanged(Domain::Task::Recurrence recurrence) { if (!m_editingInProgress) applyNewRecurrence(recurrence); } void ArtifactEditorModel::onDelegateChanged(const Domain::Task::Delegate &delegate) { m_delegateText = delegate.display(); emit delegateTextChanged(m_delegateText); } void ArtifactEditorModel::save() { if (!isSaveNeeded()) return; Q_ASSERT(m_artifact); const auto currentTitle = m_artifact->title(); m_artifact->setTitle(m_title); m_artifact->setText(m_text); if (auto task = m_artifact.objectCast()) { task->setDone(m_done); task->setStartDate(m_start); task->setDueDate(m_due); task->setRecurrence(m_recurrence); } const auto job = m_saveFunction(m_artifact); installHandler(job, i18n("Cannot modify task %1", currentTitle)); setSaveNeeded(false); } void ArtifactEditorModel::setSaveNeeded(bool needed) { if (needed) m_saveTimer->start(); else m_saveTimer->stop(); m_saveNeeded = needed; } bool ArtifactEditorModel::isSaveNeeded() const { return m_saveNeeded; } void ArtifactEditorModel::applyNewText(const QString &text) { m_text = text; emit textChanged(m_text); } void ArtifactEditorModel::applyNewTitle(const QString &title) { m_title = title; emit titleChanged(m_title); } void ArtifactEditorModel::applyNewDone(bool done) { m_done = done; emit doneChanged(m_done); } void ArtifactEditorModel::applyNewStartDate(const QDateTime &start) { m_start = start; emit startDateChanged(m_start); } void ArtifactEditorModel::applyNewDueDate(const QDateTime &due) { m_due = due; emit dueDateChanged(m_due); } void ArtifactEditorModel::applyNewRecurrence(Domain::Task::Recurrence recurrence) { m_recurrence = recurrence; emit recurrenceChanged(m_recurrence); } #include "artifacteditormodel.moc" diff --git a/src/presentation/artifacteditormodel.h b/src/presentation/artifacteditormodel.h index 7855a976..90c58d9f 100644 --- a/src/presentation/artifacteditormodel.h +++ b/src/presentation/artifacteditormodel.h @@ -1,154 +1,157 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PRESENTATION_ARTIFACTEDITORMODEL_H #define PRESENTATION_ARTIFACTEDITORMODEL_H #include #include #include #include "domain/task.h" #include "presentation/errorhandlingmodelbase.h" class QAbstractItemModel; class QTimer; namespace Presentation { class AttachmentModel; class ArtifactEditorModel : public QObject, public ErrorHandlingModelBase { Q_OBJECT Q_PROPERTY(Domain::Artifact::Ptr artifact READ artifact WRITE setArtifact NOTIFY artifactChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(bool done READ isDone WRITE setDone NOTIFY doneChanged) Q_PROPERTY(QDateTime startDate READ startDate WRITE setStartDate NOTIFY startDateChanged) Q_PROPERTY(QDateTime dueDate READ dueDate WRITE setDueDate NOTIFY dueDateChanged) Q_PROPERTY(Domain::Task::Recurrence recurrence READ recurrence WRITE setRecurrence NOTIFY recurrenceChanged) Q_PROPERTY(QAbstractItemModel* attachmentModel READ attachmentModel CONSTANT) Q_PROPERTY(QString delegateText READ delegateText NOTIFY delegateTextChanged) Q_PROPERTY(bool hasTaskProperties READ hasTaskProperties NOTIFY hasTaskPropertiesChanged) Q_PROPERTY(bool editingInProgress READ editingInProgress WRITE setEditingInProgress) public: typedef std::function SaveFunction; typedef std::function DelegateFunction; explicit ArtifactEditorModel(QObject *parent = Q_NULLPTR); ~ArtifactEditorModel(); Domain::Artifact::Ptr artifact() const; void setArtifact(const Domain::Artifact::Ptr &artifact); bool hasSaveFunction() const; void setSaveFunction(const SaveFunction &function); bool hasDelegateFunction() const; void setDelegateFunction(const DelegateFunction &function); bool hasTaskProperties() const; QString text() const; QString title() const; bool isDone() const; QDateTime startDate() const; QDateTime dueDate() const; Domain::Task::Recurrence recurrence() const; QAbstractItemModel *attachmentModel() const; QString delegateText() const; static int autoSaveDelay(); bool editingInProgress() const; public slots: void setText(const QString &text); void setTitle(const QString &title); void setDone(bool done); void setStartDate(const QDateTime &start); void setDueDate(const QDateTime &due); void setRecurrence(Domain::Task::Recurrence recurrence); void delegate(const QString &name, const QString &email); + + void addAttachment(const QString &fileName); + void removeAttachment(const QModelIndex &index); void openAttachment(const QModelIndex &index); void setEditingInProgress(bool editingInProgress); signals: void artifactChanged(const Domain::Artifact::Ptr &artifact); void hasTaskPropertiesChanged(bool hasTaskProperties); void textChanged(const QString &text); void titleChanged(const QString &title); void doneChanged(bool done); void startDateChanged(const QDateTime &date); void dueDateChanged(const QDateTime &due); void recurrenceChanged(Domain::Task::Recurrence recurrence); void delegateTextChanged(const QString &delegateText); private slots: void onTextChanged(const QString &text); void onTitleChanged(const QString &title); void onDoneChanged(bool done); void onStartDateChanged(const QDateTime &start); void onDueDateChanged(const QDateTime &due); void onRecurrenceChanged(Domain::Task::Recurrence recurrence); void onDelegateChanged(const Domain::Task::Delegate &delegate); void save(); private: void setSaveNeeded(bool needed); bool isSaveNeeded() const; void applyNewText(const QString &text); void applyNewTitle(const QString &title); void applyNewDone(bool done); void applyNewStartDate(const QDateTime &start); void applyNewDueDate(const QDateTime &due); void applyNewRecurrence(Domain::Task::Recurrence recurrence); Domain::Artifact::Ptr m_artifact; SaveFunction m_saveFunction; DelegateFunction m_delegateFunction; QString m_text; QString m_title; bool m_done; QDateTime m_start; QDateTime m_due; Domain::Task::Recurrence m_recurrence; AttachmentModel *m_attachmentModel; QString m_delegateText; QTimer *m_saveTimer; bool m_saveNeeded; bool m_editingInProgress; }; } #endif // PRESENTATION_ARTIFACTEDITORMODEL_H diff --git a/tests/units/presentation/artifacteditormodeltest.cpp b/tests/units/presentation/artifacteditormodeltest.cpp index a6e021ef..de637940 100644 --- a/tests/units/presentation/artifacteditormodeltest.cpp +++ b/tests/units/presentation/artifacteditormodeltest.cpp @@ -1,562 +1,642 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "utils/mockobject.h" #include +#include #include "testlib/fakejob.h" #include "domain/task.h" #include "domain/note.h" #include "presentation/artifacteditormodel.h" #include "presentation/errorhandler.h" using namespace mockitopp; class FakeErrorHandler : public Presentation::ErrorHandler { public: void doDisplayMessage(const QString &message) { m_message = message; } QString m_message; }; class ArtifactEditorModelTest : public QObject { Q_OBJECT public: explicit ArtifactEditorModelTest(QObject *parent = Q_NULLPTR) : QObject(parent) { qRegisterMetaType(); } private slots: void shouldHaveEmptyDefaultState() { // GIVEN Presentation::ArtifactEditorModel model; // WHEN // Nothing // THEN QVERIFY(model.artifact().isNull()); QVERIFY(!model.hasTaskProperties()); QVERIFY(model.text().isEmpty()); QVERIFY(model.title().isEmpty()); QVERIFY(!model.isDone()); QVERIFY(model.startDate().isNull()); QVERIFY(model.dueDate().isNull()); QCOMPARE(model.recurrence(), Domain::Task::NoRecurrence); QVERIFY(model.attachmentModel() != nullptr); QVERIFY(model.delegateText().isNull()); QVERIFY(!model.hasSaveFunction()); QVERIFY(!model.hasDelegateFunction()); } void shouldHaveTaskProperties() { // GIVEN Presentation::ArtifactEditorModel model; QSignalSpy textSpy(&model, &Presentation::ArtifactEditorModel::textChanged); QSignalSpy titleSpy(&model, &Presentation::ArtifactEditorModel::titleChanged); QSignalSpy doneSpy(&model, &Presentation::ArtifactEditorModel::doneChanged); QSignalSpy startSpy(&model, &Presentation::ArtifactEditorModel::startDateChanged); QSignalSpy dueSpy(&model, &Presentation::ArtifactEditorModel::dueDateChanged); QSignalSpy recurrenceSpy(&model, &Presentation::ArtifactEditorModel::recurrenceChanged); QSignalSpy attachmentSpy(model.attachmentModel(), &QAbstractItemModel::modelReset); QSignalSpy delegateSpy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); Domain::Task::Attachments attachments; Domain::Task::Attachment dataAttachment; dataAttachment.setData("foo"); dataAttachment.setLabel("dataAttachment"); dataAttachment.setMimeType("text/plain"); dataAttachment.setIconName("text-plain"); attachments.append(dataAttachment); Domain::Task::Attachment uriAttachment; uriAttachment.setUri(QUrl("https://www.kde.org")); uriAttachment.setLabel("uriAttachment"); uriAttachment.setMimeType("text/html"); uriAttachment.setIconName("text-html"); attachments.append(uriAttachment); auto task = Domain::Task::Ptr::create(); task->setText(QStringLiteral("description")); task->setTitle(QStringLiteral("title")); task->setDone(true); task->setStartDate(QDateTime::currentDateTime()); task->setDueDate(QDateTime::currentDateTime().addDays(2)); task->setRecurrence(Domain::Task::RecursDaily); task->setAttachments(attachments); task->setDelegate(Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com"))); // WHEN model.setArtifact(task); // To make sure we don't signal too much model.setText(task->text()); model.setTitle(task->title()); model.setDone(task->isDone()); model.setStartDate(task->startDate()); model.setDueDate(task->dueDate()); model.setRecurrence(task->recurrence()); // THEN QVERIFY(model.hasTaskProperties()); QCOMPARE(textSpy.size(), 1); QCOMPARE(textSpy.takeFirst().at(0).toString(), task->text()); QCOMPARE(model.property("text").toString(), task->text()); QCOMPARE(titleSpy.size(), 1); QCOMPARE(titleSpy.takeFirst().at(0).toString(), task->title()); QCOMPARE(model.property("title").toString(), task->title()); QCOMPARE(doneSpy.size(), 1); QCOMPARE(doneSpy.takeFirst().at(0).toBool(), task->isDone()); QCOMPARE(model.property("done").toBool(), task->isDone()); QCOMPARE(startSpy.size(), 1); QCOMPARE(startSpy.takeFirst().at(0).toDateTime(), task->startDate()); QCOMPARE(model.property("startDate").toDateTime(), task->startDate()); QCOMPARE(dueSpy.size(), 1); QCOMPARE(dueSpy.takeFirst().at(0).toDateTime(), task->dueDate()); QCOMPARE(model.property("dueDate").toDateTime(), task->dueDate()); QCOMPARE(recurrenceSpy.size(), 1); QCOMPARE(recurrenceSpy.takeFirst().at(0).value(), task->recurrence()); QCOMPARE(model.property("recurrence").value(), task->recurrence()); QCOMPARE(delegateSpy.size(), 1); QCOMPARE(delegateSpy.takeFirst().at(0).toString(), task->delegate().display()); QCOMPARE(model.property("delegateText").toString(), task->delegate().display()); QCOMPARE(attachmentSpy.size(), 1); auto am = model.attachmentModel(); QCOMPARE(am->rowCount(), 2); QCOMPARE(am->data(am->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("dataAttachment")); QCOMPARE(am->data(am->index(0, 0), Qt::DecorationRole).value(), QIcon::fromTheme("text-plain")); QCOMPARE(am->data(am->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("uriAttachment")); QCOMPARE(am->data(am->index(1, 0), Qt::DecorationRole).value(), QIcon::fromTheme("text-html")); } void shouldHaveNoteProperties() { // GIVEN Presentation::ArtifactEditorModel model; QSignalSpy textSpy(&model, &Presentation::ArtifactEditorModel::textChanged); QSignalSpy titleSpy(&model, &Presentation::ArtifactEditorModel::titleChanged); QSignalSpy doneSpy(&model, &Presentation::ArtifactEditorModel::doneChanged); QSignalSpy startSpy(&model, &Presentation::ArtifactEditorModel::startDateChanged); QSignalSpy dueSpy(&model, &Presentation::ArtifactEditorModel::dueDateChanged); QSignalSpy delegateSpy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); auto note = Domain::Note::Ptr::create(); note->setText(QStringLiteral("description")); note->setTitle(QStringLiteral("title")); // WHEN model.setArtifact(note); // To make sure we don't signal too much model.setText(note->text()); model.setTitle(note->title()); // THEN QVERIFY(!model.hasTaskProperties()); QCOMPARE(textSpy.size(), 1); QCOMPARE(textSpy.takeFirst().at(0).toString(), note->text()); QCOMPARE(model.property("text").toString(), note->text()); QCOMPARE(titleSpy.size(), 1); QCOMPARE(titleSpy.takeFirst().at(0).toString(), note->title()); QCOMPARE(model.property("title").toString(), note->title()); QCOMPARE(doneSpy.size(), 1); QCOMPARE(doneSpy.takeFirst().at(0).toBool(), false); QCOMPARE(model.property("done").toBool(), false); QCOMPARE(startSpy.size(), 1); QVERIFY(startSpy.takeFirst().at(0).toDateTime().isNull()); QVERIFY(model.property("startDate").toDateTime().isNull()); QCOMPARE(dueSpy.size(), 1); QVERIFY(dueSpy.takeFirst().at(0).toDateTime().isNull()); QVERIFY(model.property("dueDate").toDateTime().isNull()); QCOMPARE(delegateSpy.size(), 1); QVERIFY(delegateSpy.takeFirst().at(0).toString().isEmpty()); QVERIFY(model.property("delegateText").toString().isEmpty()); } void shouldReactToArtifactPropertyChanges_data() { QTest::addColumn("artifact"); QTest::addColumn("propertyName"); QTest::addColumn("propertyValue"); QTest::addColumn("signal"); QTest::newRow("note text") << Domain::Artifact::Ptr(Domain::Note::Ptr::create()) << QByteArray("text") << QVariant("new text") << QByteArray(SIGNAL(textChanged(QString))); QTest::newRow("note title") << Domain::Artifact::Ptr(Domain::Note::Ptr::create()) << QByteArray("title") << QVariant("new title") << QByteArray(SIGNAL(titleChanged(QString))); QTest::newRow("task text") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("text") << QVariant("new text") << QByteArray(SIGNAL(textChanged(QString))); QTest::newRow("task title") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("title") << QVariant("new title") << QByteArray(SIGNAL(titleChanged(QString))); QTest::newRow("task done") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("done") << QVariant(true) << QByteArray(SIGNAL(doneChanged(bool))); QTest::newRow("task start") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("startDate") << QVariant(QDateTime::currentDateTime()) << QByteArray(SIGNAL(startDateChanged(QDateTime))); QTest::newRow("task due") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("dueDate") << QVariant(QDateTime::currentDateTime().addDays(2)) << QByteArray(SIGNAL(dueDateChanged(QDateTime))); QTest::newRow("task recurrence") << Domain::Artifact::Ptr(Domain::Task::Ptr::create()) << QByteArray("recurrence") << QVariant::fromValue(Domain::Task::RecursDaily) << QByteArray(SIGNAL(recurrenceChanged(Domain::Task::Recurrence))); } void shouldReactToArtifactPropertyChanges() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN artifact->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); } void shouldNotReactToArtifactPropertyChangesWhenEditing_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldNotReactToArtifactPropertyChangesWhenEditing() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN const auto oldPropertyValue = artifact->property(propertyName); model.setEditingInProgress(true); artifact->setProperty(propertyName, propertyValue); // THEN QVERIFY(spy.isEmpty()); QCOMPARE(model.property(propertyName), oldPropertyValue); } void shouldReactToTaskDelegateChanges() { // GIVEN auto task = Domain::Task::Ptr::create(); Presentation::ArtifactEditorModel model; model.setArtifact(task); QSignalSpy spy(&model, &Presentation::ArtifactEditorModel::delegateTextChanged); // WHEN task->setDelegate(Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com"))); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0).toString(), task->delegate().display()); QCOMPARE(model.property("delegateText").toString(), task->delegate().display()); } void shouldApplyChangesBackToArtifactAfterADelay_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesBackToArtifactAfterADelay() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setSaveFunction(save); model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN model.setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply after delay) QTest::qWait(model.autoSaveDelay() + 50); // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldApplyChangesImmediatelyIfANewArtifactIsSet_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesImmediatelyIfANewArtifactIsSet() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setSaveFunction(save); QVERIFY(model.hasSaveFunction()); model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); // WHEN model.setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model.property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply immediately) model.setArtifact(Domain::Task::Ptr::create()); // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); savedArtifact.clear(); // WHEN (nothing else happens after a delay) QTest::qWait(model.autoSaveDelay() + 50); // THEN QVERIFY(!savedArtifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldApplyChangesImmediatelyIfDeleted_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldApplyChangesImmediatelyIfDeleted() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; return new FakeJob(this); }; auto model = new Presentation::ArtifactEditorModel; model->setSaveFunction(save); QVERIFY(model->hasSaveFunction()); model->setArtifact(artifact); QSignalSpy spy(model, signal.constData()); // WHEN model->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); QCOMPARE(spy.takeFirst().at(0), propertyValue); QCOMPARE(model->property(propertyName), propertyValue); QVERIFY(artifact->property(propertyName) != propertyValue); QVERIFY(!savedArtifact); // WHEN (apply immediately) delete model; // THEN QCOMPARE(savedArtifact, artifact); QCOMPARE(artifact->property(propertyName), propertyValue); } void shouldLaunchDelegation() { // GIVEN auto task = Domain::Task::Ptr::create(); auto expectedDelegate = Domain::Task::Delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com")); auto delegatedTask = Domain::Task::Ptr(); auto delegate = Domain::Task::Delegate(); auto delegateFunction = [this, &delegatedTask, &delegate] (const Domain::Task::Ptr &task, const Domain::Task::Delegate &d) { delegatedTask = task; delegate = d; return new FakeJob(this); }; Presentation::ArtifactEditorModel model; model.setDelegateFunction(delegateFunction); QVERIFY(model.hasDelegateFunction()); model.setArtifact(task); // WHEN model.delegate(QStringLiteral("John Doe"), QStringLiteral("john@doe.com")); // THEN QCOMPARE(delegatedTask, task); QCOMPARE(delegate, expectedDelegate); QVERIFY(!task->delegate().isValid()); } void shouldGetAnErrorMessageWhenSaveFailed() { // GIVEN auto task = Domain::Task::Ptr::create(); task->setTitle(QStringLiteral("Task 1")); auto savedArtifact = Domain::Artifact::Ptr(); auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { savedArtifact = artifact; auto job = new FakeJob(this); job->setExpectedError(KJob::KilledJobError, QStringLiteral("Foo")); return job; }; auto model = new Presentation::ArtifactEditorModel; model->setSaveFunction(save); QVERIFY(model->hasSaveFunction()); FakeErrorHandler errorHandler; model->setErrorHandler(&errorHandler); model->setArtifact(task); // WHEN model->setProperty("title", "Foo"); delete model; // THEN QTest::qWait(150); QCOMPARE(errorHandler.m_message, QStringLiteral("Cannot modify task Task 1: Foo")); } void shouldDisconnectFromPreviousArtifact_data() { shouldReactToArtifactPropertyChanges_data(); } void shouldDisconnectFromPreviousArtifact() { // GIVEN QFETCH(Domain::Artifact::Ptr, artifact); QFETCH(QByteArray, propertyName); QFETCH(QVariant, propertyValue); QFETCH(QByteArray, signal); Presentation::ArtifactEditorModel model; model.setArtifact(artifact); QSignalSpy spy(&model, signal.constData()); Domain::Artifact::Ptr newArtifact = Domain::Task::Ptr::create(); // WHEN model.setArtifact(newArtifact); // modifying the *old* artifact should have no effect. artifact->setProperty(propertyName, propertyValue); // THEN QCOMPARE(spy.size(), 1); // emitted by setArtifact QVERIFY(model.property(propertyName) != artifact->property(propertyName)); } + + void shouldAddAttachments() + { + // GIVEN + QTemporaryFile temporaryFile(QDir::tempPath() + "/artifacteditormodeltest_XXXXXX.txt"); + temporaryFile.open(); + temporaryFile.write("foo bar"); + temporaryFile.close(); + auto fileName = temporaryFile.fileName().mid(QDir::tempPath().size() + 1); + + auto task = Domain::Task::Ptr::create(); + + auto savedArtifact = Domain::Artifact::Ptr(); + auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { + savedArtifact = artifact; + return new FakeJob(this); + }; + + Presentation::ArtifactEditorModel model; + model.setSaveFunction(save); + model.setArtifact(task); + + QSignalSpy spy(model.attachmentModel(), &QAbstractItemModel::modelReset); + + // WHEN + model.addAttachment(temporaryFile.fileName()); + + // THEN + QCOMPARE(spy.size(), 1); + QCOMPARE(model.attachmentModel()->rowCount(), 1); + QVERIFY(!savedArtifact); + + // WHEN (nothing else happens after a delay) + QTest::qWait(model.autoSaveDelay() + 50); + + // THEN + QCOMPARE(savedArtifact.objectCast(), task); + QCOMPARE(task->attachments().size(), 1); + QCOMPARE(task->attachments().first().label(), fileName); + QCOMPARE(task->attachments().first().mimeType(), QStringLiteral("text/plain")); + QCOMPARE(task->attachments().first().iconName(), QStringLiteral("text-plain")); + QCOMPARE(task->attachments().first().data(), QByteArrayLiteral("foo bar")); + } + + void shouldRemoveAttachments() + { + // GIVEN + auto task = Domain::Task::Ptr::create(); + task->setAttachments(Domain::Task::Attachments() << Domain::Task::Attachment("foo") + << Domain::Task::Attachment("bar")); + + auto savedArtifact = Domain::Artifact::Ptr(); + auto save = [this, &savedArtifact] (const Domain::Artifact::Ptr &artifact) { + savedArtifact = artifact; + return new FakeJob(this); + }; + + Presentation::ArtifactEditorModel model; + model.setSaveFunction(save); + model.setArtifact(task); + + QSignalSpy spy(model.attachmentModel(), &QAbstractItemModel::modelReset); + + // WHEN + model.removeAttachment(model.attachmentModel()->index(0, 0)); + + // THEN + QCOMPARE(spy.size(), 1); + QCOMPARE(model.attachmentModel()->rowCount(), 1); + QVERIFY(!savedArtifact); + + // WHEN (nothing else happens after a delay) + QTest::qWait(model.autoSaveDelay() + 50); + + // THEN + QCOMPARE(savedArtifact.objectCast(), task); + QCOMPARE(task->attachments().size(), 1); + QCOMPARE(task->attachments().first().data(), QByteArrayLiteral("bar")); + } }; ZANSHIN_TEST_MAIN(ArtifactEditorModelTest) #include "artifacteditormodeltest.moc"