From e06e1dad4a4570e5c1181d05ab6ed7a5d74c6c91 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 3 May 2016 20:24:09 +0200 Subject: A save-as-draft action & action results This patch introduces tracking of actions, so they can be tested. It also provides a save-as-draft action, that looks for the draft folder, and stores the mail accordingly. --- framework/CMakeLists.txt | 1 + framework/actions/CMakeLists.txt | 1 + framework/actions/action.cpp | 4 +- framework/actions/action.h | 3 +- framework/actions/actionbroker.cpp | 9 +- framework/actions/actionbroker.h | 3 +- framework/actions/actionhandler.cpp | 37 +++++++- framework/actions/actionhandler.h | 14 ++- framework/actions/actionresult.cpp | 20 +++++ framework/actions/actionresult.h | 66 ++++++++++++++ framework/domain/CMakeLists.txt | 4 +- framework/domain/actions/sinkactions.cpp | 101 +++++++++++----------- framework/domain/actions/tests/CMakeLists.txt | 6 ++ framework/domain/actions/tests/sinkactiontest.cpp | 60 +++++++++++++ framework/domain/composercontroller.cpp | 5 +- framework/domain/maillistmodel.cpp | 2 +- 16 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 framework/actions/actionresult.cpp create mode 100644 framework/actions/actionresult.h create mode 100644 framework/domain/actions/tests/CMakeLists.txt create mode 100644 framework/domain/actions/tests/sinkactiontest.cpp (limited to 'framework') diff --git a/framework/CMakeLists.txt b/framework/CMakeLists.txt index c216d3c0..4fecaf14 100644 --- a/framework/CMakeLists.txt +++ b/framework/CMakeLists.txt @@ -27,6 +27,7 @@ find_package(KF5Codecs CONFIG REQUIRED) set(CMAKE_AUTOMOC ON) add_definitions("-Wall -std=c++0x -g") include_directories(.) +include_directories(SYSTEM ${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/) include_directories(SYSTEM ${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/KMime) include_directories(SYSTEM ${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/KIconThemes) diff --git a/framework/actions/CMakeLists.txt b/framework/actions/CMakeLists.txt index a09445b0..d757f654 100644 --- a/framework/actions/CMakeLists.txt +++ b/framework/actions/CMakeLists.txt @@ -3,6 +3,7 @@ set(SRCS action.cpp actionhandler.cpp actionbroker.cpp + actionresult.cpp context.cpp ) diff --git a/framework/actions/action.cpp b/framework/actions/action.cpp index e6ba2daf..28dd5e2a 100644 --- a/framework/actions/action.cpp +++ b/framework/actions/action.cpp @@ -85,8 +85,8 @@ bool Action::ready() const return ActionBroker::instance().isActionReady(mActionId, mContext); } -void Action::execute() +ActionResult Action::execute() { - ActionBroker::instance().executeAction(mActionId, mContext); + return ActionBroker::instance().executeAction(mActionId, mContext); } diff --git a/framework/actions/action.h b/framework/actions/action.h index b820955e..1ad4a47e 100644 --- a/framework/actions/action.h +++ b/framework/actions/action.h @@ -20,6 +20,7 @@ #include #include "context.h" +#include "actionresult.h" namespace Kube { @@ -43,7 +44,7 @@ public: bool ready() const; - Q_INVOKABLE void execute(); + Q_INVOKABLE ActionResult execute(); Q_SIGNALS: void readyChanged(); diff --git a/framework/actions/actionbroker.cpp b/framework/actions/actionbroker.cpp index 43a535a1..890a5566 100644 --- a/framework/actions/actionbroker.cpp +++ b/framework/actions/actionbroker.cpp @@ -54,17 +54,22 @@ bool ActionBroker::isActionReady(const QByteArray &actionId, Context *context) return false; } -void ActionBroker::executeAction(const QByteArray &actionId, Context *context) +ActionResult ActionBroker::executeAction(const QByteArray &actionId, Context *context) { if (context) { for (const auto handler : mHandler.values(actionId)) { if (handler) { - handler->execute(context); + //FIXME All handler together return one result + return handler->execute(context); } } } else { qWarning() << "Can't execute without context"; } + ActionResult result; + result.setDone(); + result.setError(1); + return result; } void ActionBroker::registerHandler(const QByteArray &actionId, ActionHandler *handler) diff --git a/framework/actions/actionbroker.h b/framework/actions/actionbroker.h index 08eac742..8f3eaeb2 100644 --- a/framework/actions/actionbroker.h +++ b/framework/actions/actionbroker.h @@ -24,6 +24,7 @@ namespace Kube { class Context; class ActionHandler; +class ActionResult; class ActionBroker : public QObject { @@ -32,7 +33,7 @@ public: static ActionBroker &instance(); bool isActionReady(const QByteArray &actionId, Context *context); - void executeAction(const QByteArray &actionId, Context *context); + ActionResult executeAction(const QByteArray &actionId, Context *context); void registerHandler(const QByteArray &actionId, ActionHandler *handler); diff --git a/framework/actions/actionhandler.cpp b/framework/actions/actionhandler.cpp index d4b01734..4ae8d0a9 100644 --- a/framework/actions/actionhandler.cpp +++ b/framework/actions/actionhandler.cpp @@ -45,17 +45,24 @@ bool ActionHandler::isActionReady(Context *context) return false; } -void ActionHandler::execute(Context *context) +ActionResult ActionHandler::execute(Context *context) { + ActionResult result; QVariant returnedValue; qWarning() << "Executing the handler"; if (context) { + //The base implementation to call the handler in QML QMetaObject::invokeMethod(this, "handler", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, QVariant::fromValue(context))); + //TODO: support async handlers in QML + result.setDone(); } else { qWarning() << "The handler didn't get a context"; + result.setDone(); + result.setError(1); } + return result; } void ActionHandler::setActionId(const QByteArray &actionId) @@ -79,12 +86,36 @@ ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsRea setActionId(actionId); } +ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &isReady, const JobHandler &handler) + : ActionHandler(nullptr), + isReadyFunction(isReady), + jobHandlerFunction(handler) +{ + setActionId(actionId); +} + bool ActionHandlerHelper::isActionReady(Context *context) { return isReadyFunction(context); } -void ActionHandlerHelper::execute(Context *context) +ActionResult ActionHandlerHelper::execute(Context *context) { - handlerFunction(context); + ActionResult result; + if (handlerFunction) { + handlerFunction(context); + result.setDone(); + } else { + jobHandlerFunction(context).then([=]() { + auto modifyableResult = result; + modifyableResult.setDone(); + }, + [=](int errorCode, const QString &string) { + qWarning() << "Job failed: " << errorCode << string; + auto modifyableResult = result; + modifyableResult.setError(1); + modifyableResult.setDone(); + }).exec(); + } + return result; } diff --git a/framework/actions/actionhandler.h b/framework/actions/actionhandler.h index 1820bfd4..c8c10dc7 100644 --- a/framework/actions/actionhandler.h +++ b/framework/actions/actionhandler.h @@ -21,6 +21,9 @@ #include #include #include +#include + +#include "actionresult.h" namespace Kube { class Context; @@ -36,7 +39,7 @@ public: virtual bool isActionReady(Context *context); // void pre(Context *context); - virtual void execute(Context *context); + virtual ActionResult execute(Context *context); // void post(Context *context); void setActionId(const QByteArray &); @@ -52,14 +55,17 @@ class ActionHandlerHelper : public ActionHandler public: typedef std::function IsReadyFunction; typedef std::function Handler; + typedef std::function(Context*)> JobHandler; ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const Handler &); + ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const JobHandler &); bool isActionReady(Context *context) Q_DECL_OVERRIDE; - void execute(Context *context) Q_DECL_OVERRIDE; + ActionResult execute(Context *context) Q_DECL_OVERRIDE; private: - const std::function isReadyFunction; - const std::function handlerFunction; + const IsReadyFunction isReadyFunction; + const Handler handlerFunction; + const JobHandler jobHandlerFunction; }; } diff --git a/framework/actions/actionresult.cpp b/framework/actions/actionresult.cpp new file mode 100644 index 00000000..631c61c4 --- /dev/null +++ b/framework/actions/actionresult.cpp @@ -0,0 +1,20 @@ +/* + Copyright (c) 2016 Christian Mollekopf + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ +#include "actionresult.h" + diff --git a/framework/actions/actionresult.h b/framework/actions/actionresult.h new file mode 100644 index 00000000..cdc6a160 --- /dev/null +++ b/framework/actions/actionresult.h @@ -0,0 +1,66 @@ +/* + Copyright (c) 2016 Christian Mollekopf + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ +#pragma once + +#include +#include + +namespace Kube { + +struct ActionResultData +{ + ActionResultData() : mError(0), mDone(false) {} + int mError; + bool mDone; +}; + +class ActionResult : public QObject +{ + Q_OBJECT +public: + ActionResult() : QObject(), mData(new ActionResultData()) {} + ActionResult(const ActionResult &rhs) : QObject(), mData(rhs.mData) {} + ActionResult &operator=(const ActionResult &rhs) + { + mData = rhs.mData; + return *this; + } + virtual ~ActionResult() {} + + void setDone() { + mData->mDone = true; + } + + bool isDone() const { + return mData->mDone; + } + + void setError(int error) { + mData->mError = error; + } + + int error() const { + return mData->mError; + } + +private: + QSharedPointer mData; +}; + +} diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index b3d47750..29385b39 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -27,7 +27,9 @@ include_directories(${CURL_INCLUDE_DIRS}) add_library(mailplugin SHARED ${mailplugin_SRCS}) qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets) -target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::MimeTreeParser KF5::Codecs KF5::Package ${CURL_LIBRARIES}) +target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::MimeTreeParser KF5::Codecs KF5::Package KF5::Async ${CURL_LIBRARIES}) + +add_subdirectory(actions/tests) install(TARGETS mailplugin DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) diff --git a/framework/domain/actions/sinkactions.cpp b/framework/domain/actions/sinkactions.cpp index dea6fc72..5dbe623c 100644 --- a/framework/domain/actions/sinkactions.cpp +++ b/framework/domain/actions/sinkactions.cpp @@ -81,16 +81,7 @@ static ActionHandlerHelper sendMailHandler("org.kde.kube.actions.sendmail", }, [](Context *context) { auto accountId = context->property("accountId").value(); - //For ssl use "smtps://mainserver.example.net - // QByteArray cacert; // = "/path/to/certificate.pem"; auto message = context->property("message").value(); - auto mimeMessage = Sink::Store::getTemporaryFilePath(); - QFile file(mimeMessage); - if (!file.open(QIODevice::ReadWrite)) { - qWarning() << "Failed to open the file: " << file.errorString() << mimeMessage; - return; - } - file.write(message->encodedContent()); qWarning() << "Sending a mail: "; Sink::Query query; @@ -98,53 +89,63 @@ static ActionHandlerHelper sendMailHandler("org.kde.kube.actions.sendmail", query += Sink::Query::PropertyFilter("account", QVariant::fromValue(accountId)); Sink::Store::fetchAll(query) .then>([=](const QList &resources) { - if (resources.isEmpty()) { - qWarning() << "Failed to find a mailtransport resource"; - } else { + if (!resources.isEmpty()) { auto resourceId = resources[0]->identifier(); qDebug() << "Sending message via resource: " << resourceId; Sink::ApplicationDomain::Mail mail(resourceId); - mail.setProperty("mimeMessage", mimeMessage); - Sink::Store::create(mail).exec(); - // return Sink::Store::create(mail); + mail.setBlobProperty("mimeMessage", message->encodedContent()); + return Sink::Store::create(mail); } + qWarning() << "Failed to find a mailtransport resource"; return KAsync::error(0, "Failed to find a MailTransport resource."); }).exec(); } ); -// static ActionHandlerHelper saveAsDraft("org.kde.kube.actions.save-as-draft", -// [](Context *context) -> bool { -// return context->property("mail").isValid(); -// }, -// [](Context *context) { -// Sink::Query query; -// query += Sink::Query::RequestedProperties(QByteArrayList() << "name") -// //FIXME do something like specialuse? -// query += Sink::Query::PropertyFilter("name", "Drafts"); -// // query += Sink::Query::PropertyContainsFilter("specialuser", "drafts"); -// query += Sink::Query::PropertyFilter("drafts", true); -// //TODO Use drafts folder of that specific account -// Sink::Store::fetchAll(query) -// .then>([](const QList folders) { -// if (folders.isEmpty()) { -// return KAsync::start([]() { -// //If message is already existing, modify, otherwise create -// }); -// } -// }); -// //TODO -// // * Find drafts folder -// // * Store KMime::Message on disk for use in blob property -// // * Check if message is already existing and either create or update -// // * -// // auto mail = context->property("mail").value(); -// // if (!mail) { -// // qWarning() << "Failed to get the mail mail: " << context->property("mail"); -// // return; -// // } -// // mail->setProperty("unread", false); -// // qDebug() << "Mark as read " << mail->identifier(); -// // Sink::Store::modify(*mail).exec(); -// } -// ); +static ActionHandlerHelper saveAsDraft("org.kde.kube.actions.save-as-draft", + [](Context *context) -> bool { + auto accountId = context->property("accountId").value(); + auto message = context->property("message").value(); + return !accountId.isEmpty() && message; + }, + ActionHandlerHelper::JobHandler([](Context *context) -> KAsync::Job { + qWarning() << "executing save as draft"; + const auto accountId = context->property("accountId").value(); + const auto message = context->property("message").value(); + auto existingMail = context->property("existingMail").value(); + if (!message) { + qWarning() << "Failed to get the mail: " << context->property("mail"); + return KAsync::error(1, "Failed to get the mail: " + context->property("mail").toString()); + } + + if (existingMail.identifier().isEmpty()) { + Sink::Query query; + query += Sink::Query::RequestedProperties(QByteArrayList() << "name"); + query += Sink::Query::PropertyContainsFilter("specialpurpose", "drafts"); + query += Sink::Query::AccountFilter(accountId); + qWarning() << "fetching the drafts folder"; + return Sink::Store::fetchAll(query) + .then>([=](const QList folders) { + qWarning() << "fetched a drafts folder" << folders.size(); + if (folders.isEmpty()) { + return KAsync::error(1, "Failed to find a drafts folder."); + } + if (folders.size() > 1) { + qWarning() << "Found too many draft folders (taking the first): " << folders; + } + const auto folder = folders.first(); + Sink::ApplicationDomain::Mail mail(folder->resourceInstanceIdentifier()); + mail.setProperty("folder", folder->identifier()); + mail.setBlobProperty("mimeMessage", message->encodedContent()); + return Sink::Store::create(mail); + }) + .then([](){ + qWarning() << "done"; + }); + } else { + qWarning() << "Modifying an existing mail"; + existingMail.setBlobProperty("mimeMessage", message->encodedContent()); + return Sink::Store::modify(existingMail); + } + }) +); diff --git a/framework/domain/actions/tests/CMakeLists.txt b/framework/domain/actions/tests/CMakeLists.txt new file mode 100644 index 00000000..dc9d01b1 --- /dev/null +++ b/framework/domain/actions/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +cmake_policy(SET CMP0063 NEW) +add_executable(sinkactiontest sinkactiontest.cpp) +add_test(sinkactiontest sinkactiontest) +qt5_use_modules(sinkactiontest Core Test Concurrent) +target_link_libraries(sinkactiontest sink actionplugin KF5::Mime mailplugin) diff --git a/framework/domain/actions/tests/sinkactiontest.cpp b/framework/domain/actions/tests/sinkactiontest.cpp new file mode 100644 index 00000000..3e4567fd --- /dev/null +++ b/framework/domain/actions/tests/sinkactiontest.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace Sink; + +class SinkActionTest : public QObject +{ + Q_OBJECT +private slots: + + void initTestCase() + { + Sink::Test::initTest(); + Sink::Log::setDebugOutputLevel(Sink::Log::Trace); + } + + void testSaveAsDraftFail() + { + Kube::Context context; + auto future = Kube::Action("org.kde.kube.actions.save-as-draft", context).execute(); + + QTRY_VERIFY(future.isDone()); + //because of empty context + QVERIFY(future.error()); + } + + void testSaveAsDraftNew() + { + auto message = KMime::Message::Ptr::create(); + message->subject(true)->fromUnicodeString(QString::fromLatin1("Foobar"), "utf8"); + message->assemble(); + + auto &&account = Test::TestAccount::registerAccount(); + auto folder = account.createEntity(); + folder->setProperty("specialpurpose", QVariant::fromValue(QByteArrayList() << "drafts")); + + Kube::Context context; + context.setProperty("message", QVariant::fromValue(message)); + context.setProperty("accountId", QVariant::fromValue(account.identifier)); + auto future = Kube::Action("org.kde.kube.actions.save-as-draft", context).execute(); + + QTRY_VERIFY(future.isDone()); + QVERIFY(!future.error()); + auto mails = account.entities(); + QCOMPARE(mails.size(), 1); + auto mail = mails.first(); + QCOMPARE(mail->getProperty("folder").toByteArray(), folder->identifier()); + } +}; + +QTEST_GUILESS_MAIN(SinkActionTest) +#include "sinkactiontest.moc" diff --git a/framework/domain/composercontroller.cpp b/framework/domain/composercontroller.cpp index bca90d33..0cf61442 100644 --- a/framework/domain/composercontroller.cpp +++ b/framework/domain/composercontroller.cpp @@ -185,9 +185,12 @@ void ComposerController::send() void ComposerController::saveAsDraft() { auto mail = assembleMessage(); + auto currentAccountId = identityModel()->index(m_currentAccountIndex, 0).data(IdentitiesModel::AccountId).toByteArray(); + Kube::Context context; context.setProperty("message", QVariant::fromValue(mail)); - Kube::Action("org.kde.kube.actions.saveasdraft", context).execute(); + context.setProperty("accountId", QVariant::fromValue(currentAccountId)); + Kube::Action("org.kde.kube.actions.save-as-draft", context).execute(); clear(); } diff --git a/framework/domain/maillistmodel.cpp b/framework/domain/maillistmodel.cpp index 7cbc5587..2df3ecbc 100644 --- a/framework/domain/maillistmodel.cpp +++ b/framework/domain/maillistmodel.cpp @@ -111,10 +111,10 @@ void MailListModel::setParentFolder(const QVariant &parentFolder) Sink::Query query; query.liveQuery = true; query.requestedProperties << "subject" << "sender" << "senderName" << "date" << "unread" << "important" << "folder"; - query.propertyFilter.insert("folder", folder->identifier()); query.resources << folder->resourceInstanceIdentifier(); query.sortProperty = "date"; query.limit = 100; + query += Sink::Query::PropertyFilter("folder", *folder); qWarning() << "Running folder query: " << folder->resourceInstanceIdentifier() << folder->identifier(); runQuery(query); } -- cgit v1.2.3