diff --git a/src/akonadi/akonadilivequeryhelpers.cpp b/src/akonadi/akonadilivequeryhelpers.cpp index 2b18eb92..40efbf72 100644 --- a/src/akonadi/akonadilivequeryhelpers.cpp +++ b/src/akonadi/akonadilivequeryhelpers.cpp @@ -1,187 +1,230 @@ /* This file is part of Zanshin Copyright 2015 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 "akonadilivequeryhelpers.h" #include "akonadi/akonadicollectionfetchjobinterface.h" #include "akonadi/akonadiitemfetchjobinterface.h" #include "akonadi/akonaditagfetchjobinterface.h" #include "utils/jobhandler.h" using namespace Akonadi; LiveQueryHelpers::LiveQueryHelpers(const SerializerInterface::Ptr &serializer, const StorageInterface::Ptr &storage) : m_serializer(serializer), m_storage(storage) { } LiveQueryHelpers::CollectionFetchFunction LiveQueryHelpers::fetchAllCollections(StorageInterface::FetchContentTypes contentTypes) const { auto storage = m_storage; return [storage, contentTypes] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchCollections(Collection::root(), StorageInterface::Recursive, contentTypes); Utils::JobHandler::install(job->kjob(), [job, add] { if (job->kjob()->error()) return; foreach (const auto &collection, job->collections()) add(collection); }); }; } LiveQueryHelpers::CollectionFetchFunction LiveQueryHelpers::fetchCollections(const Collection &root, StorageInterface::FetchContentTypes contentTypes) const { auto storage = m_storage; return [storage, contentTypes, root] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchCollections(root, StorageInterface::Recursive, contentTypes); Utils::JobHandler::install(job->kjob(), [root, job, add] { if (job->kjob()->error()) return; auto directChildren = QHash(); foreach (const auto &collection, job->collections()) { auto directChild = collection; while (directChild.parentCollection() != root) directChild = directChild.parentCollection(); if (!directChildren.contains(directChild.id())) directChildren[directChild.id()] = directChild; } foreach (const auto &directChild, directChildren) add(directChild); }); }; } LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItems(StorageInterface::FetchContentTypes contentTypes) const { auto serializer = m_serializer; auto storage = m_storage; return [serializer, storage, contentTypes] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchCollections(Akonadi::Collection::root(), StorageInterface::Recursive, contentTypes); Utils::JobHandler::install(job->kjob(), [serializer, storage, job, add] { if (job->kjob()->error() != KJob::NoError) return; foreach (const auto &collection, job->collections()) { if (!serializer->isSelectedCollection(collection)) continue; auto job = storage->fetchItems(collection); Utils::JobHandler::install(job->kjob(), [job, add] { if (job->kjob()->error() != KJob::NoError) return; foreach (const auto &item, job->items()) add(item); }); } }); }; } LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItems(const Collection &collection) const { auto storage = m_storage; return [storage, collection] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchItems(collection); Utils::JobHandler::install(job->kjob(), [job, add] { if (job->kjob()->error() != KJob::NoError) return; foreach (const auto &item, job->items()) add(item); }); }; } LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItems(const Tag &tag) const { // TODO: Qt5, use the proper implementation once we got a working akonadi #if 0 auto storage = m_storage; return [storage, tag] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchTagItems(tag); Utils::JobHandler::install(job->kjob(), [job, add] { if (job->kjob()->error() != KJob::NoError) return; foreach (const auto &item, job->items()) add(item); }); }; #else auto fetchFunction = fetchItems(StorageInterface::Tasks | StorageInterface::Notes); return [tag, fetchFunction] (const Domain::LiveQueryInput::AddFunction &add) { auto filterAdd = [tag, add] (const Item &item) { if (item.tags().contains(tag)) add(item); }; fetchFunction(filterAdd); }; #endif } +LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchTaskAndAncestors(Domain::Task::Ptr task) const +{ + Akonadi::Item childItem = m_serializer->createItemFromTask(task); + Q_ASSERT(childItem.parentCollection().isValid()); // do I really need a fetchItem first, like fetchSiblings does? + // Note: if the task moves to another collection, this live query will then be invalid... + + const Akonadi::Item::Id childId = childItem.id(); + auto storage = m_storage; + auto serializer = m_serializer; + return [storage, serializer, childItem, childId] (const Domain::LiveQueryInput::AddFunction &add) { + auto job = storage->fetchItems(childItem.parentCollection()); + Utils::JobHandler::install(job->kjob(), [job, add, serializer, childId] { + if (job->kjob()->error() != KJob::NoError) + return; + + const auto items = job->items(); + // The item itself is part of the result, we need that in findProject, to react on changes of the item itself + // To return a correct child item in case it got updated, we can't use childItem, we need to find it in the list. + const auto myself = std::find_if(items.cbegin(), items.cend(), + [childId] (const Akonadi::Item &item) { + return childId == item.id(); + }); + if (myself == items.cend()) { + qWarning() << "Did not find item in the listing for its parent collection. Item ID:" << childId; + return; + } + add(*myself); + auto parentUid = serializer->relatedUidFromItem(*myself); + while (!parentUid.isEmpty()) { + const auto parent = std::find_if(items.cbegin(), items.cend(), + [serializer, parentUid] (const Akonadi::Item &item) { + return serializer->itemUid(item) == parentUid; + }); + if (parent == items.cend()) { + break; + } + add(*parent); + parentUid = serializer->relatedUidFromItem(*parent); + } + }); + }; +} + LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchSiblings(const Item &item) const { auto storage = m_storage; return [storage, item] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchItem(item); Utils::JobHandler::install(job->kjob(), [storage, job, add] { if (job->kjob()->error() != KJob::NoError) return; Q_ASSERT(job->items().size() == 1); auto item = job->items().at(0); Q_ASSERT(item.parentCollection().isValid()); auto job = storage->fetchItems(item.parentCollection()); Utils::JobHandler::install(job->kjob(), [job, add] { if (job->kjob()->error() != KJob::NoError) return; foreach (const auto &item, job->items()) add(item); }); }); }; } LiveQueryHelpers::TagFetchFunction LiveQueryHelpers::fetchTags() const { auto storage = m_storage; return [storage] (const Domain::LiveQueryInput::AddFunction &add) { auto job = storage->fetchTags(); Utils::JobHandler::install(job->kjob(), [job, add] { foreach (const auto &tag, job->tags()) add(tag); }); }; } diff --git a/src/akonadi/akonadilivequeryhelpers.h b/src/akonadi/akonadilivequeryhelpers.h index 9b8cb7a9..eee966e6 100644 --- a/src/akonadi/akonadilivequeryhelpers.h +++ b/src/akonadi/akonadilivequeryhelpers.h @@ -1,64 +1,69 @@ /* This file is part of Zanshin Copyright 2015 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 AKONADI_LIVEQUERYHELPERS_H #define AKONADI_LIVEQUERYHELPERS_H #include "akonadi/akonadiserializerinterface.h" #include "akonadi/akonadistorageinterface.h" #include "domain/livequery.h" +#include "domain/task.h" namespace Akonadi { class LiveQueryHelpers { public: typedef QSharedPointer Ptr; typedef Domain::LiveQueryInput::FetchFunction CollectionFetchFunction; typedef Domain::LiveQueryInput::FetchFunction ItemFetchFunction; typedef Domain::LiveQueryInput::FetchFunction TagFetchFunction; LiveQueryHelpers(const SerializerInterface::Ptr &serializer, const StorageInterface::Ptr &storage); CollectionFetchFunction fetchAllCollections(StorageInterface::FetchContentTypes contentTypes) const; CollectionFetchFunction fetchCollections(const Collection &root, StorageInterface::FetchContentTypes contentTypes) const; ItemFetchFunction fetchItems(StorageInterface::FetchContentTypes contentTypes) const; ItemFetchFunction fetchItems(const Collection &collection) const; ItemFetchFunction fetchItems(const Tag &tag) const; + /// Returns a fetch function which calls a LiveQueryInput::AddFunction (provided as argument to the fetch function) + /// with the given task, then its parent, its grandparent etc. up until the project. + ItemFetchFunction fetchTaskAndAncestors(Domain::Task::Ptr task) const; + ItemFetchFunction fetchSiblings(const Item &item) const; TagFetchFunction fetchTags() const; private: SerializerInterface::Ptr m_serializer; StorageInterface::Ptr m_storage; }; } #endif // AKONADI_LIVEQUERYHELPERS_H diff --git a/src/akonadi/akonadilivequeryintegrator.h b/src/akonadi/akonadilivequeryintegrator.h index 646060bd..aeda1bcb 100644 --- a/src/akonadi/akonadilivequeryintegrator.h +++ b/src/akonadi/akonadilivequeryintegrator.h @@ -1,344 +1,387 @@ /* This file is part of Zanshin Copyright 2015 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 AKONADI_LIVEQUERYINTEGRATOR_H #define AKONADI_LIVEQUERYINTEGRATOR_H #include #include #include #include #include #include #include "akonadi/akonadimonitorinterface.h" #include "akonadi/akonadiserializerinterface.h" #include "domain/livequery.h" namespace Akonadi { class LiveQueryIntegrator : public QObject { Q_OBJECT // Helper type trait to extract parameter and return types from // a function object // Lambda or functors (via method of const method) template struct UnaryFunctionTraits : public UnaryFunctionTraits {}; // Traits definition template struct UnaryFunctionTraits { typedef Return ReturnType; typedef Arg ArgType; }; // Function pointers template struct UnaryFunctionTraits : public UnaryFunctionTraits {}; // Method template struct UnaryFunctionTraits : public UnaryFunctionTraits {}; // Const method template struct UnaryFunctionTraits : public UnaryFunctionTraits {}; // std::function object template struct UnaryFunctionTraits> : public UnaryFunctionTraits {}; // const reference to std::function object template struct UnaryFunctionTraits &> : public UnaryFunctionTraits {}; public: typedef QSharedPointer Ptr; typedef std::function CollectionRemoveHandler; typedef std::function ItemRemoveHandler; typedef std::function TagRemoveHandler; LiveQueryIntegrator(const SerializerInterface::Ptr &serializer, const MonitorInterface::Ptr &monitor, QObject *parent = Q_NULLPTR); template void bind(const QByteArray &debugName, QSharedPointer> &output, FetchFunction fetch, PredicateFunction predicate, ExtraArgs... extra) { typedef UnaryFunctionTraits FetchTraits; typedef UnaryFunctionTraits AddTraits; typedef UnaryFunctionTraits PredicateTraits; - typedef typename std::decay::type InputType; + typedef typename std::decay::type InputType; // typically Akonadi::Item static_assert(std::is_same::value, "Fetch function must return void"); static_assert(std::is_same::value, "Fetch add function must return void"); static_assert(std::is_same::value, "Predicate function must return bool"); typedef typename std::decay::type AddInputType; static_assert(std::is_same::value, "Fetch add and predicate functions must have the same input type"); if (output) return; using namespace std::placeholders; auto query = Domain::LiveQuery::Ptr::create(); query->setDebugName(debugName); query->setFetchFunction(fetch); query->setPredicateFunction(predicate); query->setConvertFunction(std::bind(&LiveQueryIntegrator::create, this, _1, extra...)); query->setUpdateFunction(std::bind(&LiveQueryIntegrator::update, this, _1, _2, extra...)); query->setRepresentsFunction(std::bind(&LiveQueryIntegrator::represents, this, _1, _2)); inputQueries() << query; output = query; } + template + void bindRelationship(const QByteArray &debugName, + QSharedPointer> &output, + FetchFunction fetch, + CompareFunction compare, + PredicateFunction predicate, + ExtraArgs... extra) + { + typedef UnaryFunctionTraits FetchTraits; + typedef UnaryFunctionTraits AddTraits; + typedef UnaryFunctionTraits PredicateTraits; + + typedef typename std::decay::type InputType; // typically Akonadi::Item + + static_assert(std::is_same::value, + "Fetch function must return void"); + static_assert(std::is_same::value, + "Fetch add function must return void"); + static_assert(std::is_same::value, + "Predicate function must return bool"); + + typedef typename std::decay::type AddInputType; + static_assert(std::is_same::value, + "Fetch add and predicate functions must have the same input type"); + + if (output) + return; + + using namespace std::placeholders; + + auto query = Domain::LiveRelationshipQuery::Ptr::create(); + + query->setDebugName(debugName); + query->setFetchFunction(fetch); + query->setCompareFunction(compare); + query->setPredicateFunction(predicate); + query->setConvertFunction(std::bind(&LiveQueryIntegrator::create, this, _1, extra...)); + query->setRepresentsFunction(std::bind(&LiveQueryIntegrator::represents, this, _1, _2)); + + inputQueries() << query; + output = query; + } + void addRemoveHandler(const CollectionRemoveHandler &handler); void addRemoveHandler(const ItemRemoveHandler &handler); void addRemoveHandler(const TagRemoveHandler &handler); private slots: void onCollectionSelectionChanged(); void onCollectionAdded(const Akonadi::Collection &collection); void onCollectionRemoved(const Akonadi::Collection &collection); void onCollectionChanged(const Akonadi::Collection &collection); void onItemAdded(const Akonadi::Item &item); void onItemRemoved(const Akonadi::Item &item); void onItemChanged(const Akonadi::Item &item); void onTagAdded(const Akonadi::Tag &tag); void onTagRemoved(const Akonadi::Tag &tag); void onTagChanged(const Akonadi::Tag &tag); private: void cleanupQueries(); template OutputType create(const InputType &input, ExtraArgs... extra); template void update(const InputType &input, OutputType &output, ExtraArgs... extra); template bool represents(const InputType &input, const OutputType &output); template typename Domain::LiveQueryInput::WeakList &inputQueries(); Domain::LiveQueryInput::WeakList m_collectionInputQueries; Domain::LiveQueryInput::WeakList m_itemInputQueries; Domain::LiveQueryInput::WeakList m_tagInputQueries; QList m_collectionRemoveHandlers; QList m_itemRemoveHandlers; QList m_tagRemoveHandlers; SerializerInterface::Ptr m_serializer; MonitorInterface::Ptr m_monitor; }; template<> inline Domain::Artifact::Ptr LiveQueryIntegrator::create(const Item &input) { return m_serializer->createArtifactFromItem(input); } template<> inline void LiveQueryIntegrator::update(const Item &input, Domain::Artifact::Ptr &output) { m_serializer->updateArtifactFromItem(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Item &input, const Domain::Artifact::Ptr &output) { return m_serializer->representsItem(output, input); } template<> inline Domain::Context::Ptr LiveQueryIntegrator::create(const Tag &input) { return m_serializer->createContextFromTag(input); } template<> inline void LiveQueryIntegrator::update(const Tag &input, Domain::Context::Ptr &output) { m_serializer->updateContextFromTag(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Tag &input, const Domain::Context::Ptr &output) { return m_serializer->isContextTag(output, input); } template<> inline Domain::DataSource::Ptr LiveQueryIntegrator::create(const Collection &input) { return m_serializer->createDataSourceFromCollection(input, SerializerInterface::BaseName); } template<> inline void LiveQueryIntegrator::update(const Collection &input, Domain::DataSource::Ptr &output) { m_serializer->updateDataSourceFromCollection(output, input, SerializerInterface::BaseName); } template<> inline Domain::DataSource::Ptr LiveQueryIntegrator::create(const Collection &input, SerializerInterface::DataSourceNameScheme nameScheme) { return m_serializer->createDataSourceFromCollection(input, nameScheme); } template<> inline void LiveQueryIntegrator::update(const Collection &input, Domain::DataSource::Ptr &output, SerializerInterface::DataSourceNameScheme nameScheme) { m_serializer->updateDataSourceFromCollection(output, input, nameScheme); } template<> inline bool LiveQueryIntegrator::represents(const Collection &input, const Domain::DataSource::Ptr &output) { return m_serializer->representsCollection(output, input); } template<> inline Domain::Note::Ptr LiveQueryIntegrator::create(const Item &input) { return m_serializer->createNoteFromItem(input); } template<> inline void LiveQueryIntegrator::update(const Item &input, Domain::Note::Ptr &output) { m_serializer->updateNoteFromItem(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Item &input, const Domain::Note::Ptr &output) { return m_serializer->representsItem(output, input); } template<> inline Domain::Project::Ptr LiveQueryIntegrator::create(const Item &input) { return m_serializer->createProjectFromItem(input); } template<> inline void LiveQueryIntegrator::update(const Item &input, Domain::Project::Ptr &output) { m_serializer->updateProjectFromItem(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Item &input, const Domain::Project::Ptr &output) { return m_serializer->representsItem(output, input); } template<> inline Domain::Tag::Ptr LiveQueryIntegrator::create(const Tag &input) { return m_serializer->createTagFromAkonadiTag(input); } template<> inline void LiveQueryIntegrator::update(const Tag &input, Domain::Tag::Ptr &output) { m_serializer->updateTagFromAkonadiTag(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Tag &input, const Domain::Tag::Ptr &output) { return m_serializer->representsAkonadiTag(output, input); } template<> inline Domain::Task::Ptr LiveQueryIntegrator::create(const Item &input) { return m_serializer->createTaskFromItem(input); } template<> inline void LiveQueryIntegrator::update(const Item &input, Domain::Task::Ptr &output) { m_serializer->updateTaskFromItem(output, input); } template<> inline bool LiveQueryIntegrator::represents(const Item &input, const Domain::Task::Ptr &output) { return m_serializer->representsItem(output, input); } template<> inline typename Domain::LiveQueryInput::WeakList &LiveQueryIntegrator::inputQueries() { return m_collectionInputQueries; } template<> inline typename Domain::LiveQueryInput::WeakList &LiveQueryIntegrator::inputQueries() { return m_itemInputQueries; } template<> inline typename Domain::LiveQueryInput::WeakList &LiveQueryIntegrator::inputQueries() { return m_tagInputQueries; } } #endif // AKONADI_LIVEQUERYINTEGRATOR_H diff --git a/src/akonadi/akonadiserializer.cpp b/src/akonadi/akonadiserializer.cpp index 3ad168ae..2872b975 100644 --- a/src/akonadi/akonadiserializer.cpp +++ b/src/akonadi/akonadiserializer.cpp @@ -1,683 +1,683 @@ /* This file is part of Zanshin Copyright 2014 Kevin Ottens Copyright 2014 Rémi Benoit 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 "akonadiserializer.h" #include #include #include #include #include #include #include #include #include "utils/mem_fn.h" #include "akonadi/akonadiapplicationselectedattribute.h" #include "akonadi/akonaditimestampattribute.h" using namespace Akonadi; Serializer::Serializer() { } Serializer::~Serializer() { } bool Serializer::representsCollection(SerializerInterface::QObjectPtr object, Collection collection) { return object->property("collectionId").toLongLong() == collection.id(); } bool Serializer::representsItem(QObjectPtr object, Item item) { return object->property("itemId").toLongLong() == item.id(); } bool Serializer::representsAkonadiTag(Domain::Tag::Ptr tag, Tag akonadiTag) const { return tag->property("tagId").toLongLong() == akonadiTag.id(); } QString Serializer::itemUid(const Item &item) { - if (isTaskItem(item)) { + if (item.hasPayload()) { const auto todo = item.payload(); return todo->uid(); } else { return QString(); } } Domain::DataSource::Ptr Serializer::createDataSourceFromCollection(Collection collection, DataSourceNameScheme naming) { if (!collection.isValid()) return Domain::DataSource::Ptr(); auto dataSource = Domain::DataSource::Ptr::create(); updateDataSourceFromCollection(dataSource, collection, naming); return dataSource; } void Serializer::updateDataSourceFromCollection(Domain::DataSource::Ptr dataSource, Collection collection, DataSourceNameScheme naming) { if (!collection.isValid()) return; QString name = collection.displayName(); if (naming == FullPath) { auto parent = collection.parentCollection(); while (parent.isValid() && parent != Akonadi::Collection::root()) { name = parent.displayName() + " » " + name; parent = parent.parentCollection(); } } dataSource->setName(name); const auto mimeTypes = collection.contentMimeTypes(); auto types = Domain::DataSource::ContentTypes(); if (mimeTypes.contains(NoteUtils::noteMimeType())) types |= Domain::DataSource::Notes; if (mimeTypes.contains(KCalCore::Todo::todoMimeType())) types |= Domain::DataSource::Tasks; dataSource->setContentTypes(types); if (collection.hasAttribute()) { auto iconName = collection.attribute()->iconName(); dataSource->setIconName(iconName); } if (!collection.hasAttribute()) { dataSource->setSelected(true); } else { auto isSelected = collection.attribute()->isSelected(); dataSource->setSelected(isSelected); } dataSource->setProperty("collectionId", collection.id()); } Collection Serializer::createCollectionFromDataSource(Domain::DataSource::Ptr dataSource) { const auto id = dataSource->property("collectionId").value(); auto collection = Collection(id); collection.attribute(Akonadi::Collection::AddIfMissing); auto selectedAttribute = collection.attribute(Akonadi::Collection::AddIfMissing); selectedAttribute->setSelected(dataSource->isSelected()); return collection; } bool Serializer::isSelectedCollection(Collection collection) { if (!isNoteCollection(collection) && !isTaskCollection(collection)) return false; if (!collection.hasAttribute()) return true; return collection.attribute()->isSelected(); } bool Akonadi::Serializer::isNoteCollection(Akonadi::Collection collection) { return collection.contentMimeTypes().contains(NoteUtils::noteMimeType()); } bool Akonadi::Serializer::isTaskCollection(Akonadi::Collection collection) { return collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType()); } bool Serializer::isTaskItem(Item item) { if (!item.hasPayload()) return false; auto todo = item.payload(); return todo->customProperty("Zanshin", "Project").isEmpty(); } Domain::Task::Ptr Serializer::createTaskFromItem(Item item) { if (!isTaskItem(item)) return Domain::Task::Ptr(); auto task = Domain::Task::Ptr::create(); updateTaskFromItem(task, item); return task; } void Serializer::updateTaskFromItem(Domain::Task::Ptr task, Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); task->setTitle(todo->summary()); task->setText(todo->description()); task->setDone(todo->isCompleted()); task->setDoneDate(todo->completed().toLocalTime().date()); task->setStartDate(todo->dtStart().toLocalTime().date()); task->setDueDate(todo->dtDue().toLocalTime().date()); task->setProperty("itemId", item.id()); task->setProperty("parentCollectionId", item.parentCollection().id()); task->setProperty("todoUid", todo->uid()); task->setProperty("relatedUid", todo->relatedTo()); task->setRunning(todo->customProperty("Zanshin", "Running") == QLatin1String("1")); switch (todo->recurrence()->recurrenceType()) { case KCalCore::Recurrence::rDaily: task->setRecurrence(Domain::Task::RecursDaily); break; case KCalCore::Recurrence::rWeekly: task->setRecurrence(Domain::Task::RecursWeekly); break; case KCalCore::Recurrence::rMonthlyDay: task->setRecurrence(Domain::Task::RecursMonthly); break; default: // Other cases are not supported for now and as such just ignored break; } QMimeDatabase mimeDb; const auto attachmentsInput = todo->attachments(); Domain::Task::Attachments attachments; attachments.reserve(attachmentsInput.size()); std::transform(attachmentsInput.cbegin(), attachmentsInput.cend(), std::back_inserter(attachments), [&mimeDb] (const KCalCore::Attachment::Ptr &attach) { Domain::Task::Attachment attachment; if (attach->isUri()) attachment.setUri(QUrl(attach->uri())); else attachment.setData(attach->decodedData()); attachment.setLabel(attach->label()); attachment.setMimeType(attach->mimeType()); attachment.setIconName(mimeDb.mimeTypeForName(attach->mimeType()).iconName()); return attachment; }); task->setAttachments(attachments); if (todo->attendeeCount() > 0) { const auto attendees = todo->attendees(); const auto delegate = std::find_if(attendees.begin(), attendees.end(), [] (const KCalCore::Attendee::Ptr &attendee) { return attendee->status() == KCalCore::Attendee::Accepted; }); if (delegate != attendees.end()) { task->setDelegate(Domain::Task::Delegate((*delegate)->name(), (*delegate)->email())); } } } bool Serializer::isTaskChild(Domain::Task::Ptr task, Akonadi::Item item) { if (!isTaskItem(item)) return false; auto todo = item.payload(); if (todo->relatedTo() == task->property("todoUid")) return true; return false; } Akonadi::Item Serializer::createItemFromTask(Domain::Task::Ptr task) { auto todo = KCalCore::Todo::Ptr::create(); todo->setSummary(task->title()); todo->setDescription(task->text()); // We only support all-day todos, so ignore timezone information and possible effect from timezone on dates // KCalCore reads "DUE;VALUE=DATE:20171130" as QDateTime(QDate(2017, 11, 30), QTime(), Qt::LocalTime), for lack of timezone information // so we should never call toUtc() on that, it would mess up the date. // If one day we want to support time information, we need to add a task->isAllDay()/setAllDay(). todo->setDtStart(QDateTime(task->startDate())); todo->setDtDue(QDateTime(task->dueDate())); todo->setAllDay(true); if (task->property("todoUid").isValid()) { todo->setUid(task->property("todoUid").toString()); } if (task->property("relatedUid").isValid()) { todo->setRelatedTo(task->property("relatedUid").toString()); } switch (task->recurrence()) { case Domain::Task::NoRecurrence: break; case Domain::Task::RecursDaily: todo->recurrence()->setDaily(1); break; case Domain::Task::RecursWeekly: todo->recurrence()->setWeekly(1); break; case Domain::Task::RecursMonthly: todo->recurrence()->setMonthly(1); break; } for (const auto &attachment : task->attachments()) { KCalCore::Attachment::Ptr attach(new KCalCore::Attachment(QByteArray())); if (attachment.isUri()) attach->setUri(attachment.uri().toString()); else attach->setDecodedData(attachment.data()); attach->setMimeType(attachment.mimeType()); attach->setLabel(attachment.label()); todo->addAttachment(attach); } if (task->delegate().isValid()) { KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(task->delegate().name(), task->delegate().email(), true, KCalCore::Attendee::Accepted)); todo->addAttendee(attendee); } if (task->isRunning()) { todo->setCustomProperty("Zanshin", "Running", "1"); } else { todo->removeCustomProperty("Zanshin", "Running"); } // Needs to be done after all other dates are positioned // since this applies the recurrence logic if (task->isDone()) todo->setCompleted(QDateTime(task->doneDate(), QTime(), Qt::UTC)); else todo->setCompleted(false); Akonadi::Item item; if (task->property("itemId").isValid()) { item.setId(task->property("itemId").value()); } if (task->property("parentCollectionId").isValid()) { auto parentId = task->property("parentCollectionId").value(); item.setParentCollection(Akonadi::Collection(parentId)); } item.setMimeType(KCalCore::Todo::todoMimeType()); item.setPayload(todo); return item; } QString Serializer::relatedUidFromItem(Akonadi::Item item) { if (isTaskItem(item)) { const auto todo = item.payload(); return todo->relatedTo(); } else if (isNoteItem(item)) { const auto message = item.payload(); const auto relatedHeader = message->headerByType("X-Zanshin-RelatedProjectUid"); return relatedHeader ? relatedHeader->asUnicodeString() : QString(); } else { return QString(); } } void Serializer::updateItemParent(Akonadi::Item item, Domain::Task::Ptr parent) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(parent->property("todoUid").toString()); } void Serializer::updateItemProject(Item item, Domain::Project::Ptr project) { if (isTaskItem(item)) { auto todo = item.payload(); todo->setRelatedTo(project->property("todoUid").toString()); } else if (isNoteItem(item)) { auto note = item.payload(); note->removeHeader("X-Zanshin-RelatedProjectUid"); const QByteArray parentUid = project->property("todoUid").toString().toUtf8(); if (!parentUid.isEmpty()) { auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(parentUid); note->appendHeader(relatedHeader); } note->assemble(); } } void Serializer::removeItemParent(Akonadi::Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(QString()); } void Serializer::promoteItemToProject(Akonadi::Item item) { if (!isTaskItem(item)) return; auto todo = item.payload(); todo->setRelatedTo(QString()); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); } void Serializer::clearItem(Akonadi::Item *item) { Q_ASSERT(item); if (!isTaskItem(*item)) return; // NOTE : Currently not working, when akonadistorage test will make it pass, we will use it // item->clearTags(); foreach (const Tag& tag, item->tags()) item->clearTag(tag); } Akonadi::Item::List Serializer::filterDescendantItems(const Akonadi::Item::List &potentialChildren, const Akonadi::Item &ancestorItem) { if (potentialChildren.isEmpty()) return Akonadi::Item::List(); Akonadi::Item::List itemsToProcess = potentialChildren; Q_ASSERT(ancestorItem.isValid() && ancestorItem.hasPayload()); KCalCore::Todo::Ptr todo = ancestorItem.payload(); const auto bound = std::partition(itemsToProcess.begin(), itemsToProcess.end(), [ancestorItem, todo](Akonadi::Item currentItem) { return (!currentItem.hasPayload() || currentItem == ancestorItem || currentItem.payload()->relatedTo() != todo->uid()); }); Akonadi::Item::List itemsRemoved; itemsRemoved.reserve(std::distance(itemsToProcess.begin(), bound)); std::copy(itemsToProcess.begin(), bound, std::back_inserter(itemsRemoved)); itemsToProcess.erase(itemsToProcess.begin(), bound); auto result = std::accumulate(itemsToProcess.begin(), itemsToProcess.end(), Akonadi::Item::List(), [this, itemsRemoved](Akonadi::Item::List result, Akonadi::Item currentItem) { result << currentItem; return result += filterDescendantItems(itemsRemoved, currentItem); }); return result; } bool Serializer::isNoteItem(Item item) { return item.hasPayload(); } Domain::Note::Ptr Serializer::createNoteFromItem(Akonadi::Item item) { if (!isNoteItem(item)) return Domain::Note::Ptr(); Domain::Note::Ptr note = Domain::Note::Ptr::create(); updateNoteFromItem(note, item); return note; } void Serializer::updateNoteFromItem(Domain::Note::Ptr note, Item item) { if (!isNoteItem(item)) return; auto message = item.payload(); note->setTitle(message->subject(true)->asUnicodeString()); note->setText(message->mainBodyPart()->decodedText()); note->setProperty("itemId", item.id()); if (auto relatedHeader = message->headerByType("X-Zanshin-RelatedProjectUid")) { note->setProperty("relatedUid", relatedHeader->asUnicodeString()); } else { note->setProperty("relatedUid", QVariant()); } } Item Serializer::createItemFromNote(Domain::Note::Ptr note) { NoteUtils::NoteMessageWrapper builder; builder.setTitle(note->title()); builder.setText(note->text() + '\n'); // Adding an extra '\n' because KMime always removes it... KMime::Message::Ptr message = builder.message(); if (!note->property("relatedUid").toString().isEmpty()) { auto relatedHeader = new KMime::Headers::Generic("X-Zanshin-RelatedProjectUid"); relatedHeader->from7BitString(note->property("relatedUid").toString().toUtf8()); message->appendHeader(relatedHeader); } Akonadi::Item item; if (note->property("itemId").isValid()) { item.setId(note->property("itemId").value()); } item.setMimeType(Akonadi::NoteUtils::noteMimeType()); item.setPayload(message); return item; } bool Serializer::isProjectItem(Item item) { if (!item.hasPayload()) return false; return !isTaskItem(item); } Domain::Project::Ptr Serializer::createProjectFromItem(Item item) { if (!isProjectItem(item)) return Domain::Project::Ptr(); auto project = Domain::Project::Ptr::create(); updateProjectFromItem(project, item); return project; } void Serializer::updateProjectFromItem(Domain::Project::Ptr project, Item item) { if (!isProjectItem(item)) return; auto todo = item.payload(); project->setName(todo->summary()); project->setProperty("itemId", item.id()); project->setProperty("parentCollectionId", item.parentCollection().id()); project->setProperty("todoUid", todo->uid()); } Item Serializer::createItemFromProject(Domain::Project::Ptr project) { auto todo = KCalCore::Todo::Ptr::create(); todo->setSummary(project->name()); todo->setCustomProperty("Zanshin", "Project", QStringLiteral("1")); if (project->property("todoUid").isValid()) { todo->setUid(project->property("todoUid").toString()); } Akonadi::Item item; if (project->property("itemId").isValid()) { item.setId(project->property("itemId").value()); } if (project->property("parentCollectionId").isValid()) { auto parentId = project->property("parentCollectionId").value(); item.setParentCollection(Akonadi::Collection(parentId)); } item.setMimeType(KCalCore::Todo::todoMimeType()); item.setPayload(todo); return item; } bool Serializer::isProjectChild(Domain::Project::Ptr project, Item item) { const QString todoUid = project->property("todoUid").toString(); const QString relatedUid = relatedUidFromItem(item); return !todoUid.isEmpty() && !relatedUid.isEmpty() && todoUid == relatedUid; } Domain::Context::Ptr Serializer::createContextFromTag(Akonadi::Tag tag) { if (!isContext(tag)) return Domain::Context::Ptr(); auto context = Domain::Context::Ptr::create(); updateContextFromTag(context, tag); return context; } Akonadi::Tag Serializer::createTagFromContext(Domain::Context::Ptr context) { auto tag = Akonadi::Tag(); tag.setName(context->name()); tag.setType(Akonadi::SerializerInterface::contextTagType()); tag.setGid(QByteArray(context->name().toLatin1())); if (context->property("tagId").isValid()) tag.setId(context->property("tagId").value()); return tag; } void Serializer::updateContextFromTag(Domain::Context::Ptr context, Akonadi::Tag tag) { if (!isContext(tag)) return; context->setProperty("tagId", tag.id()); context->setName(tag.name()); } bool Serializer::hasContextTags(Item item) const { using namespace std::placeholders; Tag::List tags = item.tags(); return std::any_of(tags.constBegin(), tags.constEnd(), std::bind(Utils::mem_fn(&Serializer::isContext), this, _1)); } bool Serializer::hasAkonadiTags(Item item) const { using namespace std::placeholders; Tag::List tags = item.tags(); return std::any_of(tags.constBegin(), tags.constEnd(), std::bind(Utils::mem_fn(&Serializer::isAkonadiTag), this, _1)); } bool Serializer::isContext(const Akonadi::Tag &tag) const { return (tag.type() == Akonadi::SerializerInterface::contextTagType()); } bool Serializer::isAkonadiTag(const Tag &tag) const { return tag.type() == Akonadi::Tag::PLAIN; } bool Serializer::isContextTag(const Domain::Context::Ptr &context, const Akonadi::Tag &tag) const { return (context->property("tagId").value() == tag.id()); } bool Serializer::isContextChild(Domain::Context::Ptr context, Item item) const { if (!context->property("tagId").isValid()) return false; auto tagId = context->property("tagId").value(); Akonadi::Tag tag(tagId); return item.hasTag(tag); } Domain::Tag::Ptr Serializer::createTagFromAkonadiTag(Akonadi::Tag akonadiTag) { if (!isAkonadiTag(akonadiTag)) return Domain::Tag::Ptr(); auto tag = Domain::Tag::Ptr::create(); updateTagFromAkonadiTag(tag, akonadiTag); return tag; } void Serializer::updateTagFromAkonadiTag(Domain::Tag::Ptr tag, Akonadi::Tag akonadiTag) { if (!isAkonadiTag(akonadiTag)) return; tag->setProperty("tagId", akonadiTag.id()); tag->setName(akonadiTag.name()); } Akonadi::Tag Serializer::createAkonadiTagFromTag(Domain::Tag::Ptr tag) { auto akonadiTag = Akonadi::Tag(); akonadiTag.setName(tag->name()); akonadiTag.setType(Akonadi::Tag::PLAIN); akonadiTag.setGid(QByteArray(tag->name().toLatin1())); const auto tagProperty = tag->property("tagId"); if (tagProperty.isValid()) akonadiTag.setId(tagProperty.value()); return akonadiTag; } bool Serializer::isTagChild(Domain::Tag::Ptr tag, Akonadi::Item item) { if (!tag->property("tagId").isValid()) return false; auto tagId = tag->property("tagId").value(); Akonadi::Tag akonadiTag(tagId); return item.hasTag(akonadiTag); } diff --git a/src/akonadi/akonaditaskqueries.cpp b/src/akonadi/akonaditaskqueries.cpp index 0247a674..d06665a3 100644 --- a/src/akonadi/akonaditaskqueries.cpp +++ b/src/akonadi/akonaditaskqueries.cpp @@ -1,176 +1,191 @@ /* 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 "akonaditaskqueries.h" #include "utils/datetime.h" #include using namespace Akonadi; TaskQueries::TaskQueries(const StorageInterface::Ptr &storage, const SerializerInterface::Ptr &serializer, const MonitorInterface::Ptr &monitor, const Cache::Ptr &cache) : m_serializer(serializer), m_cache(cache), m_helpers(new LiveQueryHelpers(serializer, storage)), m_integrator(new LiveQueryIntegrator(serializer, monitor)), m_workdayPollTimer(new QTimer(this)) { m_workdayPollTimer->setInterval(30000); connect(m_workdayPollTimer, &QTimer::timeout, this, &TaskQueries::onWorkdayPollTimeout); m_integrator->addRemoveHandler([this] (const Item &item) { m_findChildren.remove(item.id()); }); } int TaskQueries::workdayPollInterval() const { return m_workdayPollTimer->interval(); } void TaskQueries::setWorkdayPollInterval(int interval) { m_workdayPollTimer->setInterval(interval); } TaskQueries::TaskResult::Ptr TaskQueries::findAll() const { auto fetch = m_helpers->fetchItems(StorageInterface::Tasks); auto predicate = [this] (const Akonadi::Item &item) { return m_serializer->isTaskItem(item); }; m_integrator->bind("TaskQueries::findAll", m_findAll, fetch, predicate); return m_findAll->result(); } TaskQueries::TaskResult::Ptr TaskQueries::findChildren(Domain::Task::Ptr task) const { Akonadi::Item item = m_serializer->createItemFromTask(task); auto &query = m_findChildren[item.id()]; auto fetch = m_helpers->fetchSiblings(item); auto predicate = [this, task] (const Akonadi::Item &childItem) { return m_serializer->isTaskChild(task, childItem); }; m_integrator->bind("TaskQueries::findChildren", query, fetch, predicate); return query->result(); } +TaskQueries::ProjectResult::Ptr TaskQueries::findProject(Domain::Task::Ptr task) const +{ + Akonadi::Item childItem = m_serializer->createItemFromTask(task); + auto &query = m_findProject[childItem.id()]; + auto fetch = m_helpers->fetchTaskAndAncestors(task); + auto predicate = [this, childItem] (const Akonadi::Item &item) { + return m_serializer->isProjectItem(item); + }; + auto compare = [] (const Akonadi::Item &item1, const Akonadi::Item &item2) { + return item1.id() == item2.id(); + }; + m_integrator->bindRelationship("TaskQueries::findProject", query, fetch, compare, predicate); + return query->result(); +} + TaskQueries::TaskResult::Ptr TaskQueries::findTopLevel() const { auto fetch = m_helpers->fetchItems(StorageInterface::Tasks); auto predicate = [this] (const Akonadi::Item &item) { return m_serializer->relatedUidFromItem(item).isEmpty() && m_serializer->isTaskItem(item); }; m_integrator->bind("TaskQueries::findTopLevel", m_findTopLevel, fetch, predicate); return m_findTopLevel->result(); } TaskQueries::TaskResult::Ptr TaskQueries::findInboxTopLevel() const { auto fetch = m_helpers->fetchItems(StorageInterface::Tasks); auto predicate = [this] (const Akonadi::Item &item) { const bool excluded = !m_serializer->isTaskItem(item) || !m_serializer->relatedUidFromItem(item).isEmpty(); return !excluded; }; m_integrator->bind("TaskQueries::findInboxTopLevel", m_findInboxTopLevel, fetch, predicate); return m_findInboxTopLevel->result(); } TaskQueries::TaskResult::Ptr TaskQueries::findWorkdayTopLevel() const { if (!m_findWorkdayTopLevel) { m_workdayPollTimer->start(); m_today = Utils::DateTime::currentDate(); } auto fetch = m_helpers->fetchItems(StorageInterface::Tasks); auto isWorkdayItem = [this] (const Akonadi::Item &item) { if (!m_serializer->isTaskItem(item)) return false; const Domain::Task::Ptr task = m_serializer->createTaskFromItem(item); const QDate doneDate = task->doneDate(); const QDate startDate = task->startDate(); const QDate dueDate = task->dueDate(); const QDate today = Utils::DateTime::currentDate(); const bool pastStartDate = startDate.isValid() && startDate <= today; const bool pastDueDate = dueDate.isValid() && dueDate <= today; const bool todayDoneDate = doneDate == today; if (task->isDone()) return todayDoneDate; else return pastStartDate || pastDueDate; }; auto predicate = [this, isWorkdayItem] (const Akonadi::Item &item) { if (!isWorkdayItem(item)) return false; const auto items = m_cache->items(item.parentCollection()); auto currentItem = item; auto parentUid = m_serializer->relatedUidFromItem(currentItem); while (!parentUid.isEmpty()) { const auto parent = std::find_if(items.cbegin(), items.cend(), [this, parentUid] (const Akonadi::Item &item) { return m_serializer->itemUid(item) == parentUid; }); if (parent == items.cend()) break; if (isWorkdayItem(*parent)) return false; currentItem = *parent; parentUid = m_serializer->relatedUidFromItem(currentItem); } return true; }; m_integrator->bind("TaskQueries::findWorkdayTopLevel", m_findWorkdayTopLevel, fetch, predicate); return m_findWorkdayTopLevel->result(); } TaskQueries::ContextResult::Ptr TaskQueries::findContexts(Domain::Task::Ptr task) const { qFatal("Not implemented yet"); Q_UNUSED(task); return ContextResult::Ptr(); } void TaskQueries::onWorkdayPollTimeout() { auto newDate = Utils::DateTime::currentDate(); if (m_findWorkdayTopLevel && m_today != newDate) { m_today = newDate; m_findWorkdayTopLevel->reset(); } } diff --git a/src/akonadi/akonaditaskqueries.h b/src/akonadi/akonaditaskqueries.h index c6da651b..0eb2f10b 100644 --- a/src/akonadi/akonaditaskqueries.h +++ b/src/akonadi/akonaditaskqueries.h @@ -1,86 +1,91 @@ /* 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 AKONADI_TASKQUERIES_H #define AKONADI_TASKQUERIES_H #include "domain/taskqueries.h" #include "akonadi/akonadicache.h" #include "akonadi/akonadilivequeryhelpers.h" #include "akonadi/akonadilivequeryintegrator.h" class QTimer; namespace Akonadi { class TaskQueries : public QObject, public Domain::TaskQueries { Q_OBJECT public: typedef QSharedPointer Ptr; typedef Domain::LiveQueryInput ItemInputQuery; typedef Domain::LiveQueryOutput TaskQueryOutput; typedef Domain::QueryResultProvider TaskProvider; typedef Domain::QueryResult TaskResult; typedef Domain::QueryResultProvider ContextProvider; typedef Domain::QueryResult ContextResult; + typedef Domain::QueryResult ProjectResult; + typedef Domain::LiveQueryOutput ProjectQueryOutput; + TaskQueries(const StorageInterface::Ptr &storage, const SerializerInterface::Ptr &serializer, const MonitorInterface::Ptr &monitor, const Cache::Ptr &cache); int workdayPollInterval() const; void setWorkdayPollInterval(int interval); TaskResult::Ptr findAll() const Q_DECL_OVERRIDE; TaskResult::Ptr findChildren(Domain::Task::Ptr task) const Q_DECL_OVERRIDE; TaskResult::Ptr findTopLevel() const Q_DECL_OVERRIDE; TaskResult::Ptr findInboxTopLevel() const Q_DECL_OVERRIDE; TaskResult::Ptr findWorkdayTopLevel() const Q_DECL_OVERRIDE; ContextResult::Ptr findContexts(Domain::Task::Ptr task) const Q_DECL_OVERRIDE; + ProjectResult::Ptr findProject(Domain::Task::Ptr task) const Q_DECL_OVERRIDE; private slots: void onWorkdayPollTimeout(); private: SerializerInterface::Ptr m_serializer; Cache::Ptr m_cache; LiveQueryHelpers::Ptr m_helpers; LiveQueryIntegrator::Ptr m_integrator; QTimer *m_workdayPollTimer; mutable QDate m_today; mutable TaskQueryOutput::Ptr m_findAll; mutable QHash m_findChildren; + mutable QHash m_findProject; mutable TaskQueryOutput::Ptr m_findTopLevel; mutable TaskQueryOutput::Ptr m_findInboxTopLevel; mutable TaskQueryOutput::Ptr m_findWorkdayTopLevel; }; } #endif // AKONADI_TASKQUERIES_H diff --git a/src/domain/livequery.h b/src/domain/livequery.h index 85fb91b9..bfb76256 100644 --- a/src/domain/livequery.h +++ b/src/domain/livequery.h @@ -1,269 +1,434 @@ /* 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 DOMAIN_LIVEQUERY_H #define DOMAIN_LIVEQUERY_H #include "queryresult.h" namespace Domain { template class LiveQueryInput { public: typedef QSharedPointer> Ptr; typedef QWeakPointer> WeakPtr; typedef QList List; typedef QList WeakList; typedef std::function AddFunction; typedef std::function FetchFunction; typedef std::function PredicateFunction; virtual ~LiveQueryInput() {} virtual void reset() = 0; virtual void onAdded(const InputType &input) = 0; virtual void onChanged(const InputType &input) = 0; virtual void onRemoved(const InputType &input) = 0; }; template class LiveQueryOutput { public: typedef QSharedPointer> Ptr; typedef QList List; typedef QueryResult Result; virtual ~LiveQueryOutput() {} virtual typename Result::Ptr result() = 0; virtual void reset() = 0; }; template class LiveQuery : public LiveQueryInput, public LiveQueryOutput { public: typedef QSharedPointer> Ptr; typedef QList List; typedef QueryResultProvider Provider; typedef QueryResult Result; typedef typename LiveQueryInput::AddFunction AddFunction; typedef typename LiveQueryInput::FetchFunction FetchFunction; typedef typename LiveQueryInput::PredicateFunction PredicateFunction; typedef std::function ConvertFunction; typedef std::function UpdateFunction; typedef std::function RepresentsFunction; LiveQuery() = default; LiveQuery(const LiveQuery &other) = default; LiveQuery &operator=(const LiveQuery &other) = default; ~LiveQuery() { clear(); } typename Result::Ptr result() Q_DECL_OVERRIDE { typename Provider::Ptr provider(m_provider.toStrongRef()); if (provider) return Result::create(provider); provider = Provider::Ptr::create(); m_provider = provider.toWeakRef(); doFetch(); return Result::create(provider); } void setFetchFunction(const FetchFunction &fetch) { m_fetch = fetch; } void setPredicateFunction(const PredicateFunction &predicate) { m_predicate = predicate; } void setConvertFunction(const ConvertFunction &convert) { m_convert = convert; } void setUpdateFunction(const UpdateFunction &update) { m_update = update; } void setDebugName(const QByteArray &name) { m_debugName = name; } void setRepresentsFunction(const RepresentsFunction &represents) { m_represents = represents; } void reset() Q_DECL_OVERRIDE { clear(); doFetch(); } void onAdded(const InputType &input) Q_DECL_OVERRIDE { typename Provider::Ptr provider(m_provider.toStrongRef()); if (!provider) return; if (m_predicate(input)) addToProvider(provider, input); } void onChanged(const InputType &input) Q_DECL_OVERRIDE { typename Provider::Ptr provider(m_provider.toStrongRef()); if (!provider) return; if (!m_predicate(input)) { for (int i = 0; i < provider->data().size(); i++) { auto output = provider->data().at(i); if (m_represents(input, output)) { provider->removeAt(i); i--; } } } else { bool found = false; for (int i = 0; i < provider->data().size(); i++) { auto output = provider->data().at(i); if (m_represents(input, output)) { m_update(input, output); provider->replace(i, output); found = true; } } if (!found) addToProvider(provider, input); } } void onRemoved(const InputType &input) Q_DECL_OVERRIDE { typename Provider::Ptr provider(m_provider.toStrongRef()); if (!provider) return; for (int i = 0; i < provider->data().size(); i++) { auto output = provider->data().at(i); if (m_represents(input, output)) { provider->removeAt(i); i--; } } } private: template bool isValidOutput(const T &/*output*/) { return true; } template bool isValidOutput(const QSharedPointer &output) { return !output.isNull(); } template bool isValidOutput(T *output) { return output != Q_NULLPTR; } void addToProvider(const typename Provider::Ptr &provider, const InputType &input) { auto output = m_convert(input); if (isValidOutput(output)) provider->append(output); } void doFetch() { typename Provider::Ptr provider(m_provider.toStrongRef()); if (!provider) return; auto addFunction = [this, provider] (const InputType &input) { if (m_predicate(input)) addToProvider(provider, input); }; m_fetch(addFunction); } void clear() { typename Provider::Ptr provider(m_provider.toStrongRef()); if (!provider) return; while (!provider->data().isEmpty()) provider->removeFirst(); } FetchFunction m_fetch; PredicateFunction m_predicate; ConvertFunction m_convert; UpdateFunction m_update; RepresentsFunction m_represents; QByteArray m_debugName; typename Provider::WeakPtr m_provider; }; +// A query that stores an intermediate list of results (from the fetch), to react on changes on any item in that list +// and then filters that list with the predicate for the final result +// When one of the intermediary items changes, a full fetch is done again. +template +class LiveRelationshipQuery : public LiveQueryInput, public LiveQueryOutput +{ +public: + typedef QSharedPointer> Ptr; + typedef QList List; + + typedef QueryResultProvider Provider; + typedef QueryResult Result; + + typedef typename LiveQueryInput::AddFunction AddFunction; + typedef typename LiveQueryInput::FetchFunction FetchFunction; + typedef typename LiveQueryInput::PredicateFunction PredicateFunction; + + typedef std::function ConvertFunction; + typedef std::function RepresentsFunction; + typedef std::function CompareFunction; + + LiveRelationshipQuery() = default; + LiveRelationshipQuery(const LiveRelationshipQuery &other) = default; + LiveRelationshipQuery &operator=(const LiveRelationshipQuery &other) = default; + + ~LiveRelationshipQuery() + { + clear(); + } + + typename Result::Ptr result() Q_DECL_OVERRIDE + { + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (provider) + return Result::create(provider); + provider = Provider::Ptr::create(); + m_provider = provider.toWeakRef(); + + doFetch(); + + return Result::create(provider); + } + + void setFetchFunction(const FetchFunction &fetch) + { + m_fetch = fetch; + } + + void setPredicateFunction(const PredicateFunction &predicate) + { + m_predicate = predicate; + } + + void setCompareFunction(const CompareFunction &compare) + { + m_compare = compare; + } + + void setConvertFunction(const ConvertFunction &convert) + { + m_convert = convert; + } + + void setDebugName(const QByteArray &name) + { + m_debugName = name; + } + + void setRepresentsFunction(const RepresentsFunction &represents) + { + m_represents = represents; + } + + void reset() Q_DECL_OVERRIDE + { + clear(); + doFetch(); + } + + void onAdded(const InputType &input) Q_DECL_OVERRIDE + { + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (!provider) + return; + + m_intermediaryResults.append(input); + if (m_predicate(input)) + addToProvider(provider, input); + } + + void onChanged(const InputType &input) Q_DECL_OVERRIDE + { + Q_ASSERT(m_compare); + const bool found = std::any_of(m_intermediaryResults.constBegin(), m_intermediaryResults.constEnd(), + [&input, this](const InputType &existing) { + return m_compare(input, existing); + }); + if (found) + reset(); + } + + void onRemoved(const InputType &input) Q_DECL_OVERRIDE + { + onChanged(input); + } + +private: + template + bool isValidOutput(const T &/*output*/) + { + return true; + } + + template + bool isValidOutput(const QSharedPointer &output) + { + return !output.isNull(); + } + + template + bool isValidOutput(T *output) + { + return output != Q_NULLPTR; + } + + void addToProvider(const typename Provider::Ptr &provider, const InputType &input) + { + auto output = m_convert(input); + if (isValidOutput(output)) + provider->append(output); + } + + void doFetch() + { + auto addFunction = [this] (const InputType &input) { + onAdded(input); + }; + m_fetch(addFunction); + } + + void clear() + { + m_intermediaryResults.clear(); + + typename Provider::Ptr provider(m_provider.toStrongRef()); + + if (!provider) + return; + + while (!provider->data().isEmpty()) + provider->removeFirst(); + } + + FetchFunction m_fetch; + PredicateFunction m_predicate; + ConvertFunction m_convert; + CompareFunction m_compare; + RepresentsFunction m_represents; + QByteArray m_debugName; + + typename Provider::WeakPtr m_provider; + QList m_intermediaryResults; +}; } #endif // DOMAIN_LIVEQUERY_H diff --git a/src/domain/taskqueries.h b/src/domain/taskqueries.h index 7fed0d20..c5bd18f8 100644 --- a/src/domain/taskqueries.h +++ b/src/domain/taskqueries.h @@ -1,56 +1,59 @@ /* 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 DOMAIN_TASKQUERIES_H #define DOMAIN_TASKQUERIES_H #include "context.h" #include "queryresult.h" #include "task.h" +#include "project.h" namespace Domain { class TaskQueries { public: typedef QSharedPointer Ptr; TaskQueries(); virtual ~TaskQueries(); virtual QueryResult::Ptr findAll() const = 0; virtual QueryResult::Ptr findChildren(Task::Ptr task) const = 0; virtual QueryResult::Ptr findTopLevel() const = 0; virtual QueryResult::Ptr findInboxTopLevel() const = 0; virtual QueryResult::Ptr findWorkdayTopLevel() const = 0; virtual QueryResult::Ptr findContexts(Task::Ptr task) const = 0; + + virtual QueryResult::Ptr findProject(Task::Ptr task) const = 0; }; } #endif // DOMAIN_TASKQUERIES_H diff --git a/src/presentation/workdaypagemodel.cpp b/src/presentation/workdaypagemodel.cpp index be12515b..a9a51580 100644 --- a/src/presentation/workdaypagemodel.cpp +++ b/src/presentation/workdaypagemodel.cpp @@ -1,189 +1,205 @@ /* This file is part of Zanshin Copyright 2015 Theo Vaucher 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 "workdaypagemodel.h" #include #include #include "domain/noterepository.h" #include "domain/taskqueries.h" #include "domain/taskrepository.h" #include "presentation/querytreemodel.h" #include "utils/datetime.h" using namespace Presentation; WorkdayPageModel::WorkdayPageModel(const Domain::TaskQueries::Ptr &taskQueries, const Domain::TaskRepository::Ptr &taskRepository, QObject *parent) : PageModel(parent), m_taskQueries(taskQueries), m_taskRepository(taskRepository) { } Domain::Artifact::Ptr WorkdayPageModel::addItem(const QString &title, const QModelIndex &parentIndex) { const auto parentData = parentIndex.data(QueryTreeModel::ObjectRole); const auto parentArtifact = parentData.value(); const auto parentTask = parentArtifact.objectCast(); auto task = Domain::Task::Ptr::create(); task->setTitle(title); if (!parentTask) task->setStartDate(Utils::DateTime::currentDate()); const auto job = parentTask ? m_taskRepository->createChild(task, parentTask) : m_taskRepository->create(task); installHandler(job, i18n("Cannot add task %1 in Workday", title)); return task; } void WorkdayPageModel::removeItem(const QModelIndex &index) { QVariant data = index.data(QueryTreeModel::ObjectRole); auto artifact = data.value(); auto task = artifact.objectCast(); if (task) { const auto job = m_taskRepository->remove(task); installHandler(job, i18n("Cannot remove task %1 from Workday", task->title())); } } void WorkdayPageModel::promoteItem(const QModelIndex &index) { QVariant data = index.data(QueryTreeModel::ObjectRole); auto artifact = data.value(); auto task = artifact.objectCast(); Q_ASSERT(task); const auto job = m_taskRepository->promoteToProject(task); installHandler(job, i18n("Cannot promote task %1 to be a project", task->title())); } QAbstractItemModel *WorkdayPageModel::createCentralListModel() { auto query = [this](const Domain::Artifact::Ptr &artifact) -> Domain::QueryResultInterface::Ptr { if (!artifact) return Domain::QueryResult::copy(m_taskQueries->findWorkdayTopLevel()); else if (auto task = artifact.dynamicCast()) return Domain::QueryResult::copy(m_taskQueries->findChildren(task)); else return Domain::QueryResult::Ptr(); }; auto flags = [](const Domain::Artifact::Ptr &artifact) { const Qt::ItemFlags defaultFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; return artifact.dynamicCast() ? (defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled) : defaultFlags; }; - auto data = [](const Domain::Artifact::Ptr &artifact, int role) -> QVariant { - if (role != Qt::DisplayRole - && role != Qt::EditRole - && role != Qt::CheckStateRole) { - return QVariant(); - } - - if (role == Qt::DisplayRole || role == Qt::EditRole) { - return artifact->title(); - } else if (auto task = artifact.dynamicCast()) { - return task->isDone() ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); + auto data = [this](const Domain::Artifact::Ptr &artifact, int role) -> QVariant { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return artifact->title(); + case Qt::CheckStateRole: + if (auto task = artifact.dynamicCast()) { + return task->isDone() ? Qt::Checked : Qt::Unchecked; + } + break; + case ProjectRole: + case Qt::ToolTipRole: + if (auto task = artifact.dynamicCast()) { + static Domain::QueryResult::Ptr lastProjectResult; + auto projectResult = m_taskQueries->findProject(task); + if (projectResult) { + // keep a refcount to it, for next time we get here... + lastProjectResult = projectResult; + if (!projectResult->data().isEmpty()) { + Domain::Project::Ptr project = projectResult->data().at(0); + return i18n("Project: %1", project->name()); + } + } + return i18n("Inbox"); + } + break; + default: + break; } + return QVariant(); }; auto setData = [this](const Domain::Artifact::Ptr &artifact, const QVariant &value, int role) { if (role != Qt::EditRole && role != Qt::CheckStateRole) { return false; } if (auto task = artifact.dynamicCast()) { const auto currentTitle = task->title(); if (role == Qt::EditRole) task->setTitle(value.toString()); else task->setDone(value.toInt() == Qt::Checked); const auto job = m_taskRepository->update(task); installHandler(job, i18n("Cannot modify task %1 in Workday", currentTitle)); return true; } return false; }; auto drop = [this](const QMimeData *mimeData, Qt::DropAction, const Domain::Artifact::Ptr &artifact) { auto parentTask = artifact.objectCast(); if (!mimeData->hasFormat(QStringLiteral("application/x-zanshin-object"))) return false; auto droppedArtifacts = mimeData->property("objects").value(); if (droppedArtifacts.isEmpty()) return false; if (std::any_of(droppedArtifacts.begin(), droppedArtifacts.end(), [](const Domain::Artifact::Ptr &droppedArtifact) { return !droppedArtifact.objectCast(); })) { return false; } foreach(const auto &droppedArtifact, droppedArtifacts) { auto childTask = droppedArtifact.objectCast(); if (parentTask) { const auto job = m_taskRepository->associate(parentTask, childTask); installHandler(job, i18n("Cannot move task %1 as sub-task of %2", childTask->title(), parentTask->title())); } else { childTask->setStartDate(Utils::DateTime::currentDate()); auto job = m_taskRepository->dissociate(childTask); installHandler(job, i18n("Cannot deparent task %1 from its parent", childTask->title())); } } return true; }; auto drag = [](const Domain::Artifact::List &artifacts) -> QMimeData* { if (artifacts.isEmpty()) return Q_NULLPTR; auto data = new QMimeData; data->setData(QStringLiteral("application/x-zanshin-object"), "object"); data->setProperty("objects", QVariant::fromValue(artifacts)); return data; }; return new QueryTreeModel(query, flags, data, setData, drop, drag, this); } diff --git a/src/presentation/workdaypagemodel.h b/src/presentation/workdaypagemodel.h index a4cf340d..f1c0abb2 100644 --- a/src/presentation/workdaypagemodel.h +++ b/src/presentation/workdaypagemodel.h @@ -1,56 +1,57 @@ /* This file is part of Zanshin Copyright 2015 Theo Vaucher 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_WORKDAYPAGEMODEL_H #define PRESENTATION_WORKDAYPAGEMODEL_H #include "presentation/pagemodel.h" #include "domain/taskqueries.h" #include "domain/taskrepository.h" namespace Presentation { class WorkdayPageModel : public PageModel { Q_OBJECT public: + enum { ProjectRole = 0x386F4B }; explicit WorkdayPageModel(const Domain::TaskQueries::Ptr &taskQueries, const Domain::TaskRepository::Ptr &taskRepository, QObject *parent = Q_NULLPTR); Domain::Artifact::Ptr addItem(const QString &title, const QModelIndex &parentIndex = QModelIndex()) Q_DECL_OVERRIDE; void removeItem(const QModelIndex &index) Q_DECL_OVERRIDE; void promoteItem(const QModelIndex &index) Q_DECL_OVERRIDE; private: QAbstractItemModel *createCentralListModel() Q_DECL_OVERRIDE; Domain::TaskQueries::Ptr m_taskQueries; Domain::TaskRepository::Ptr m_taskRepository; }; } #endif // PRESENTATION_WORKDAYPAGEMODEL_H diff --git a/tests/units/akonadi/akonaditaskqueriestest.cpp b/tests/units/akonadi/akonaditaskqueriestest.cpp index a5233720..66066c5a 100644 --- a/tests/units/akonadi/akonaditaskqueriestest.cpp +++ b/tests/units/akonadi/akonaditaskqueriestest.cpp @@ -1,1623 +1,1819 @@ /* 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 "akonadi/akonadicachingstorage.h" #include "akonadi/akonaditaskqueries.h" #include "akonadi/akonadiserializer.h" +#include "akonadi/akonadiitemfetchjobinterface.h" #include "testlib/akonadifakedata.h" #include "testlib/gencollection.h" #include "testlib/gennote.h" #include "testlib/gentag.h" #include "testlib/gentodo.h" #include "testlib/testhelpers.h" #include "utils/datetime.h" using namespace Testlib; class AkonadiTaskQueriesTest : public QObject { Q_OBJECT private: Akonadi::StorageInterface::Ptr createCachingStorage(AkonadiFakeData &data, const Akonadi::Cache::Ptr &cache) { auto storage = Akonadi::StorageInterface::Ptr(data.createStorage()); return Akonadi::StorageInterface::Ptr(new Akonadi::CachingStorage(cache, storage)); } private slots: void shouldLookInAllReportedForAllTasks() { // GIVEN AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // Two tasks in the second collection data.createItem(GenTodo().withId(43).withParent(43).withTitle(QStringLiteral("43"))); data.createItem(GenTodo().withId(44).withParent(43).withTitle(QStringLiteral("44"))); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findAll(); result->data(); result = queries->findAll(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 3); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); QCOMPARE(result->data().at(2)->title(), QStringLiteral("44")); } void shouldIgnoreItemsWhichAreNotTasks() { // GIVEN AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Two items in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // One of them is not a task auto item = Akonadi::Item(43); item.setPayloadFromData("FooBar"); item.setParentCollection(Akonadi::Collection(42)); data.createItem(item); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findAll(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldReactToItemAddsForTasksOnly() { // GIVEN AkonadiFakeData data; // One empty collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findAll(); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); // WHEN data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); auto item = Akonadi::Item(43); item.setPayloadFromData("FooBar"); item.setParentCollection(Akonadi::Collection(42)); data.createItem(item); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().first()->title(), QStringLiteral("42")); } void shouldReactToItemRemovesForAllTasks() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three task in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); data.createItem(GenTodo().withId(43).withParent(42).withTitle(QStringLiteral("43"))); data.createItem(GenTodo().withId(44).withParent(42).withTitle(QStringLiteral("44"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findAll(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 3); // WHEN data.removeItem(Akonadi::Item(43)); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); } void shouldReactToItemChangesForAllTasks() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three task in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); data.createItem(GenTodo().withId(43).withParent(42).withTitle(QStringLiteral("43"))); data.createItem(GenTodo().withId(44).withParent(42).withTitle(QStringLiteral("44"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findAll(); // Even though the pointer didn't change it's convenient to user if we call // the replace handlers bool replaceHandlerCalled = false; result->addPostReplaceHandler([&replaceHandlerCalled](const Domain::Task::Ptr &, int) { replaceHandlerCalled = true; }); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 3); // WHEN data.modifyItem(GenTodo(data.item(43)).withTitle(QStringLiteral("43bis"))); // THEN QCOMPARE(result->data().size(), 3); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43bis")); QCOMPARE(result->data().at(2)->title(), QStringLiteral("44")); QVERIFY(replaceHandlerCalled); } void shouldLookInAllChildrenReportedForAllChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); // WHEN auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); result->data(); result = queries->findChildren(task); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); // Should not change nothing result = queries->findChildren(task); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); } void shouldNotCrashWhenWeAskAgainTheSameChildrenList() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One task in the collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); // The bug we're trying to hit here is the following: // - when findChildren is called the first time a provider is created internally // - result is deleted at the end of the loop, no one holds the provider with // a strong reference anymore so it is deleted as well // - when findChildren is called the second time, there's a risk of a dangling // pointer if the recycling of providers is wrongly implemented which can lead // to a crash, if it is properly done no crash will occur for (int i = 0; i < 2; i++) { // WHEN * 2 auto result = queries->findChildren(task); // THEN * 2 QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); } } void shouldReactToItemAddsForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One task in the collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); // WHEN data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); } void shouldReactToItemChangesForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); bool replaceHandlerCalled = false; result->addPostReplaceHandler([&replaceHandlerCalled](const Domain::Task::Ptr &, int) { replaceHandlerCalled = true; }); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.modifyItem(GenTodo(data.item(43)).withTitle(QStringLiteral("43bis"))); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43bis")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); QVERIFY(replaceHandlerCalled); } void shouldRemoveItemFromCorrespondingResultWhenRelatedItemChangeForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.modifyItem(GenTodo(data.item(43)).withParentUid(QLatin1String(""))); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("44")); } void shouldAddItemToCorrespondingResultWhenRelatedItemChangeForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being top level) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); bool replaceHandlerCalled = false; result->addPostReplaceHandler([&replaceHandlerCalled](const Domain::Task::Ptr &, int) { replaceHandlerCalled = true; }); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); // WHEN data.modifyItem(GenTodo(data.item(43)).withParentUid(QStringLiteral("uid-42"))); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("44")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); QVERIFY(!replaceHandlerCalled); } void shouldMoveItemToCorrespondingResultWhenRelatedItemChangeForChildTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being top level) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task1 = serializer->createTaskFromItem(data.item(42)); auto task2 = serializer->createTaskFromItem(data.item(43)); auto result1 = queries->findChildren(task1); auto result2 = queries->findChildren(task2); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result1->data().size(), 1); QCOMPARE(result1->data().at(0)->title(), QStringLiteral("44")); QCOMPARE(result2->data().size(), 0); // WHEN data.modifyItem(GenTodo(data.item(44)).withParentUid(QStringLiteral("uid-43"))); // THEN QCOMPARE(result1->data().size(), 0); QCOMPARE(result2->data().size(), 1); QCOMPARE(result2->data().at(0)->title(), QStringLiteral("44")); } void shouldReactToItemRemovesForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto task = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.removeItem(Akonadi::Item(43)); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("44")); } void shouldLookInAllReportedForTopLevelTasks() { // GIVEN AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // Two tasks in the second collection data.createItem(GenTodo().withId(43).withParent(43).withTitle(QStringLiteral("43"))); data.createItem(GenTodo().withId(44).withParent(43).withTitle(QStringLiteral("44")).withParentUid(QStringLiteral("2"))); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); result->data(); result = queries->findTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldReactToItemAddsForTopLevelTasks() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); // WHEN data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); data.createItem(GenTodo().withId(43).withParent(42).withTitle(QStringLiteral("43")).withParentUid(QStringLiteral("2"))); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().first()->title(), QStringLiteral("42")); } void shouldReactToItemRemovesForTopLevelTasks() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (one being child of the second one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.removeItem(Akonadi::Item(43)); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldReactToItemChangesForTopLevelTasks() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (one being child of the second one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); // Even though the pointer didn't change it's convenient to user if we call // the replace handlers bool replaceHandlerCalled = false; result->addPostReplaceHandler([&replaceHandlerCalled](const Domain::Task::Ptr &, int) { replaceHandlerCalled = true; }); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.modifyItem(GenTodo(data.item(43)).withTitle(QStringLiteral("43bis"))); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43bis")); QVERIFY(replaceHandlerCalled); } void shouldRemoveItemFromTopLevelResultWhenRelatedItemChangeForTopLevelTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (one being child of the second one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); // WHEN data.modifyItem(GenTodo(data.item(43)).withParentUid(QStringLiteral("uid-42"))); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldAddItemToTopLevelResultWhenRelatedItemChangeForChildrenTask() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (one being child of the second one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); // WHEN data.modifyItem(GenTodo(data.item(43)).withParentUid(QString())); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldRemoveParentNodeAndMoveChildrenInTopLevelResult() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (one being child of the second one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); auto resultChild = queries->findChildren(result->data().at(1)); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(resultChild->data().size(), 1); // WHEN data.removeItem(Akonadi::Item(43)); // THEN QCOMPARE(resultChild->data().size(), 0); QCOMPARE(result->data().size(), 1); // FIXME: Should become 2 once we got a proper cache in place } void shouldNotCrashDuringFindChildrenWhenJobIsKilled() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); data.storageBehavior().setFetchItemErrorCode(42, KJob::KilledJobError); // WHEN auto task1 = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task1); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); } void shouldNotCrashDuringFindChildrenWhenItemsJobReceiveResult_data() { QTest::addColumn("errorCode"); QTest::addColumn("fetchBehavior"); QTest::addColumn("deleteQuery"); QTest::newRow("No error with empty list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << false; QTest::newRow("Error with empty list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << true; QTest::newRow("Error with list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::NormalFetch) << true; } void shouldNotCrashDuringFindChildrenWhenItemsJobReceiveResult() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (two being children of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto storage = Akonadi::StorageInterface::Ptr(data.createStorage()); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto monitor = Akonadi::MonitorInterface::Ptr(data.createMonitor()); QScopedPointer queries(new Akonadi::TaskQueries(storage, serializer, monitor, Akonadi::Cache::Ptr())); QFETCH(int, errorCode); QFETCH(int, fetchBehavior); QFETCH(bool, deleteQuery); data.storageBehavior().setFetchItemsErrorCode(42, errorCode); data.storageBehavior().setFetchItemsBehavior(42, AkonadiFakeStorageBehavior::FetchBehavior(fetchBehavior)); // WHEN auto task1 = serializer->createTaskFromItem(data.item(42)); auto result = queries->findChildren(task1); if (deleteQuery) delete queries.take(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); } void shouldNotCrashDuringFindAllWhenFetchJobFailedOrEmpty_data() { QTest::addColumn("colErrorCode"); QTest::addColumn("colFetchBehavior"); QTest::addColumn("itemsErrorCode"); QTest::addColumn("itemsFetchBehavior"); QTest::addColumn("deleteQuery"); QTest::newRow("No error with empty collection list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << false; QTest::newRow("Error with empty collection list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << true; QTest::newRow("Error with collection list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << true; QTest::newRow("No error with empty item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << false; QTest::newRow("Error with empty item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << false; QTest::newRow("Error with item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::NormalFetch) << false; } void shouldNotCrashDuringFindAllWhenFetchJobFailedOrEmpty() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44"))); auto storage = Akonadi::StorageInterface::Ptr(data.createStorage()); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto monitor = Akonadi::MonitorInterface::Ptr(data.createMonitor()); QScopedPointer queries(new Akonadi::TaskQueries(storage, serializer, monitor, Akonadi::Cache::Ptr())); QFETCH(int, colErrorCode); QFETCH(int, colFetchBehavior); data.storageBehavior().setFetchCollectionsErrorCode(Akonadi::Collection::root().id(), colErrorCode); data.storageBehavior().setFetchCollectionsBehavior(Akonadi::Collection::root().id(), AkonadiFakeStorageBehavior::FetchBehavior(colFetchBehavior)); QFETCH(int, itemsErrorCode); QFETCH(int, itemsFetchBehavior); data.storageBehavior().setFetchItemsErrorCode(42, itemsErrorCode); data.storageBehavior().setFetchItemsBehavior(42, AkonadiFakeStorageBehavior::FetchBehavior(itemsFetchBehavior)); QFETCH(bool, deleteQuery); // WHEN auto result = queries->findAll(); if (deleteQuery) delete queries.take(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); } void shouldNotCrashDuringFindTopLevelWhenFetchJobFailedOrEmpty_data() { QTest::addColumn("colErrorCode"); QTest::addColumn("colFetchBehavior"); QTest::addColumn("itemsErrorCode"); QTest::addColumn("itemsFetchBehavior"); QTest::addColumn("deleteQuery"); QTest::newRow("No error with empty collection list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << false; QTest::newRow("Error with empty collection list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << true; QTest::newRow("Error with collection list") << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << true; QTest::newRow("No error with empty item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << false; QTest::newRow("Error with empty item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::EmptyFetch) << false; QTest::newRow("Error with item list") << int(KJob::NoError) << int(AkonadiFakeStorageBehavior::NormalFetch) << int(KJob::KilledJobError) << int(AkonadiFakeStorageBehavior::NormalFetch) << false; } void shouldNotCrashDuringFindTopLevelWhenFetchJobFailedOrEmpty() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three tasks in the collection (third one being child of the first one) data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(42) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-42"))); auto storage = Akonadi::StorageInterface::Ptr(data.createStorage()); auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto monitor = Akonadi::MonitorInterface::Ptr(data.createMonitor()); QScopedPointer queries(new Akonadi::TaskQueries(storage, serializer, monitor, Akonadi::Cache::Ptr())); QFETCH(int, colErrorCode); QFETCH(int, colFetchBehavior); data.storageBehavior().setFetchCollectionsErrorCode(Akonadi::Collection::root().id(), colErrorCode); data.storageBehavior().setFetchCollectionsBehavior(Akonadi::Collection::root().id(), AkonadiFakeStorageBehavior::FetchBehavior(colFetchBehavior)); QFETCH(int, itemsErrorCode); QFETCH(int, itemsFetchBehavior); data.storageBehavior().setFetchItemsErrorCode(42, itemsErrorCode); data.storageBehavior().setFetchItemsBehavior(42, AkonadiFakeStorageBehavior::FetchBehavior(itemsFetchBehavior)); QFETCH(bool, deleteQuery); // WHEN auto result = queries->findTopLevel(); if (deleteQuery) delete queries.take(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); } void shouldIgnoreProjectsWhenReportingTopLevelTasks() { // GIVEN AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); // Two tasks and one project in the second collection data.createItem(GenTodo().withId(43).withParent(43) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(44).withParent(43) .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) .withParentUid(QStringLiteral("uid-43"))); data.createItem(GenTodo().withId(45).withParent(43) .withTitle(QStringLiteral("45")).withUid(QStringLiteral("uid-45")) .asProject()); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findTopLevel(); result->data(); result = queries->findTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldLookInAllSelectedCollectionsForInboxTopLevel() { // GIVEN AkonadiFakeData data; // Three top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withNoteContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(44).withRootAsParent().withTaskContent().selected(false)); // One note in the first collection data.createItem(GenNote().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // Two tasks in the second collection data.createItem(GenTodo().withId(43).withParent(43).withTitle(QStringLiteral("43"))); data.createItem(GenTodo().withId(44).withParent(43).withTitle(QStringLiteral("44"))); // One task in the third collection data.createItem(GenTodo().withId(45).withParent(44).withTitle(QStringLiteral("45"))); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); result->data(); result = queries->findInboxTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("44")); } void shouldIgnoreItemsWhichAreNotTasksInInboxTopLevel() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Two items in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // One of them is not a task auto item = Akonadi::Item(43); item.setPayloadFromData("FooBar"); item.setParentCollection(Akonadi::Collection(42)); data.createItem(item); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldNotHaveTasksWithParentsInInboxTopLevel() { // TODO: Note that this specification is kind of an over simplification which // assumes that all the underlying data is correct. Ideally it should be checked // that the uid referred to actually points to a todo which exists in a proper // collection. We will need a cache to be able to implement that properly though. // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // Three items in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(43).withParent(42).withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")).withParentUid(QStringLiteral("uid-42"))); data.createItem(GenTodo().withId(44).withParent(42).withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")).withParentUid(QStringLiteral("foo"))); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldHaveTasksWithContextsInInboxTopLevel_data() { QTest::addColumn("hasContexts"); QTest::addColumn("isExpectedInInbox"); QTest::newRow("task with no context") << false << true; QTest::newRow("task with contexts") << true << true; } void shouldHaveTasksWithContextsInInboxTopLevel() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One context tag data.createTag(GenTag().withId(42).withName(QStringLiteral("tag-42")).asContext()); // One item in the collection QFETCH(bool, hasContexts); auto tagIds = QList(); if (hasContexts) tagIds << 42; data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42")).withTags(tagIds)); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QFETCH(bool, isExpectedInInbox); if (isExpectedInInbox) { QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } else { QVERIFY(result->data().isEmpty()); } } void shouldReactToItemAddsForInboxTopLevel_data() { QTest::addColumn("reactionExpected"); QTest::addColumn("relatedUid"); QTest::addColumn("hasContexts"); QTest::newRow("task which should be in inbox") << true << QString() << false; QTest::newRow("task with related uid") << false << "foo" << false; QTest::newRow("task with context") << true << QString() << true; } void shouldReactToItemAddsForInboxTopLevel() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One context tag data.createTag(GenTag().withId(42).withName(QStringLiteral("tag-42")).asContext()); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); TestHelpers::waitForEmptyJobQueue(); QVERIFY(result->data().isEmpty()); // WHEN QFETCH(QString, relatedUid); QFETCH(bool, hasContexts); auto tagIds = QList(); if (hasContexts) tagIds << 42; data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42")).withTags(tagIds).withParentUid(relatedUid)); // THEN QFETCH(bool, reactionExpected); if (reactionExpected) { QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } else { QVERIFY(result->data().isEmpty()); } } void shouldReactToItemRemovesForInboxTopLevel() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One item in the collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); // WHEN QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().first()->title(), QStringLiteral("42")); // WHEN data.removeItem(Akonadi::Item(42)); // THEN QVERIFY(result->data().isEmpty()); } void shouldReactToItemChangesForInboxTopLevel_data() { QTest::addColumn("inListAfterChange"); QTest::addColumn("relatedUidBefore"); QTest::addColumn("relatedUidAfter"); QTest::newRow("task appears in inbox (related uid)") << true << "foo" << QString(); QTest::newRow("task disappears from inbox (related uid)") << false << QString() << "foo"; } void shouldReactToItemChangesForInboxTopLevel() { // GIVEN AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One context tag data.createTag(GenTag().withId(42).withName(QStringLiteral("tag-42")).asContext()); // Artifact data QFETCH(QString, relatedUidBefore); data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42")).withParentUid(relatedUidBefore)); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); TestHelpers::waitForEmptyJobQueue(); QFETCH(bool, inListAfterChange); if (inListAfterChange) { QVERIFY(result->data().isEmpty()); } else { QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } // WHEN QFETCH(QString, relatedUidAfter); data.modifyItem(GenTodo(data.item(42)).withParentUid(relatedUidAfter)); // THEN if (inListAfterChange) { QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } else { QVERIFY(result->data().isEmpty()); } } void shouldReactToCollectionSelectionChangesForInboxTopLevel() { // GIVEN AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in each collection data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); data.createItem(GenTodo().withId(43).withParent(43).withTitle(QStringLiteral("43"))); QScopedPointer queries(new Akonadi::TaskQueries(Akonadi::StorageInterface::Ptr(data.createStorage()), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), Akonadi::Cache::Ptr())); auto result = queries->findInboxTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); // WHEN data.modifyCollection(GenCollection(data.collection(43)).selected(false)); TestHelpers::waitForEmptyJobQueue(); // THEN QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); } void shouldLookInAllWorkdayReportedForAllTasks_data() { QTest::addColumn("isExpectedInWorkday"); QTest::addColumn("item2"); const auto today = Utils::DateTime::currentDate(); QTest::newRow("todayTask") << true << Akonadi::Item(GenTodo() .withStartDate(today) .withDueDate(today)); QTest::newRow("pastTask") << true << Akonadi::Item(GenTodo() .withStartDate(today.addDays(-42)) .withDueDate(today.addDays(-41))); QTest::newRow("startTodayTask") << true << Akonadi::Item(GenTodo() .withStartDate(today)); QTest::newRow("endTodayTask") << true << Akonadi::Item(GenTodo() .withStartDate(today)); QTest::newRow("futureTask") << false << Akonadi::Item(GenTodo() .withStartDate(today.addDays(41)) .withDueDate(today.addDays(42))); QTest::newRow("pastDoneTask") << false << Akonadi::Item(GenTodo() .withStartDate(today.addDays(-42)) .withDueDate(today.addDays(-41)) .done() .withDoneDate(today.addDays(-30))); QTest::newRow("todayDoneTask") << true << Akonadi::Item(GenTodo() .withStartDate(today) .withDueDate(today) .done() .withDoneDate(today)); QTest::newRow("startTodayDoneTask") << true << Akonadi::Item(GenTodo() .withStartDate(today) .done() .withDoneDate(today)); QTest::newRow("endTodayDoneTask") << true << Akonadi::Item(GenTodo() .withDueDate(today) .done() .withDoneDate(today)); } void shouldLookInAllWorkdayReportedForAllTasks() { // GIVEN const auto today = Utils::DateTime::currentDate(); AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42")) .withStartDate(today)); // One task in the second collection (from data driven set) QFETCH(Akonadi::Item, item2); data.createItem(GenTodo(item2).withId(43).withParent(43) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43"))); // WHEN auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); QScopedPointer queries(new Akonadi::TaskQueries(createCachingStorage(data, cache), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), cache)); auto result = queries->findWorkdayTopLevel(); result->data(); result = queries->findWorkdayTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QFETCH(bool, isExpectedInWorkday); const int sizeExpected = (isExpectedInWorkday) ? 2 : 1; QCOMPARE(result->data().size(), sizeExpected); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); if (isExpectedInWorkday) QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldLookInAllWorkdayReportedForAllTasksWhenOverrideDate() { // GIVEN qputenv("ZANSHIN_OVERRIDE_DATE", "2015-03-10"); const auto today = Utils::DateTime::currentDate(); AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42")) .withStartDate(today)); // One task in the second collection data.createItem(GenTodo().withId(43).withParent(43) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withStartDate(today)); // WHEN auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); QScopedPointer queries(new Akonadi::TaskQueries(createCachingStorage(data, cache), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), cache)); auto result = queries->findWorkdayTopLevel(); result->data(); result = queries->findWorkdayTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldPollForCurrentDayToListWorkday() { // GIVEN qputenv("ZANSHIN_OVERRIDE_DATE", "2015-03-10"); const auto today = Utils::DateTime::currentDate(); AkonadiFakeData data; // Two top level collections data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); // One task in the first collection data.createItem(GenTodo().withId(42).withParent(42) .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42")) .withStartDate(today)); // One task in the second collection data.createItem(GenTodo().withId(43).withParent(43) .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) .withStartDate(today.addDays(1))); QScopedPointer queries; { auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); auto akqueries = new Akonadi::TaskQueries(createCachingStorage(data, cache), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), cache); QCOMPARE(akqueries->workdayPollInterval(), 30000); akqueries->setWorkdayPollInterval(500); queries.reset(akqueries); } auto result = queries->findWorkdayTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); // WHEN qputenv("ZANSHIN_OVERRIDE_DATE", "2015-03-11"); QTest::qWait(1000); TestHelpers::waitForEmptyJobQueue(); // THEN QCOMPARE(result->data().size(), 2); QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); QCOMPARE(result->data().at(1)->title(), QStringLiteral("43")); } void shouldNotListWorkdayTasksTwiceIfTheyHaveAParentInWorkday() { // GIVEN const auto today = Utils::DateTime::currentDate(); AkonadiFakeData data; // One top level collection data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); // One context tag data.createTag(GenTag().withId(42).withName(QStringLiteral("42")).asContext()); // Five tasks in the collection, two start today, three not, all forming an ancestry line data.createItem(GenTodo().withParent(42).withId(42).withTitle(QStringLiteral("42")).withUid("42")); data.createItem(GenTodo().withParent(42).withId(43).withTitle(QStringLiteral("43")).withUid("43").withParentUid("42").withStartDate(today)); data.createItem(GenTodo().withParent(42).withId(44).withTitle(QStringLiteral("44")).withUid("44").withParentUid("43")); data.createItem(GenTodo().withParent(42).withId(45).withTitle(QStringLiteral("45")).withUid("45").withParentUid("44").withStartDate(today)); data.createItem(GenTodo().withParent(42).withId(46).withTitle(QStringLiteral("46")).withUid("46").withParentUid("45")); // WHEN auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); QScopedPointer queries(new Akonadi::TaskQueries(createCachingStorage(data, cache), Akonadi::Serializer::Ptr(new Akonadi::Serializer), Akonadi::MonitorInterface::Ptr(data.createMonitor()), cache)); auto result = queries->findWorkdayTopLevel(); result->data(); result = queries->findWorkdayTopLevel(); // Should not cause any problem or wrong data // THEN QVERIFY(result->data().isEmpty()); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); // Should not change anything result = queries->findWorkdayTopLevel(); TestHelpers::waitForEmptyJobQueue(); QCOMPARE(result->data().size(), 1); QCOMPARE(result->data().at(0)->title(), QStringLiteral("43")); } + + void findProjectShouldLookInCollection() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + auto collection = GenCollection().withId(42).withRootAsParent().withTaskContent(); + data.createCollection(collection); + + // Three tasks in the collection (two being children of the first one) + data.createItem(GenTodo().withId(42).asProject().withParent(42) + .withTitle(QStringLiteral("42")).withUid(QStringLiteral("uid-42"))); + data.createItem(GenTodo().withId(43).withParent(42) + .withTitle(QStringLiteral("43")).withUid(QStringLiteral("uid-43")) + .withParentUid(QStringLiteral("uid-42"))); + data.createItem(GenTodo().withId(44).withParent(42) + .withTitle(QStringLiteral("44")).withUid(QStringLiteral("uid-44")) + .withParentUid(QStringLiteral("uid-42"))); + + // WHEN + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(44)); + // populate cache for collection + auto *fetchJob = storage->fetchItems(collection); + QVERIFY2(fetchJob->kjob()->exec(), qPrintable(fetchJob->kjob()->errorString())); + + auto result = queries->findProject(task); + + // THEN + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("42")); + + // Should not change anything + result = queries->findProject(task); + + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("42")); + } + + void findProjectShouldReactToRelationshipChange() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + const Akonadi::Collection::Id colId = 42; + auto collection = GenCollection().withId(colId).withRootAsParent().withTaskContent(); + data.createCollection(collection); + + // Three tasks in the collection (two being children of the first one) + // 1->2->3 (project) where 1 changes to 1->4->5 (project) + data.createItem(GenTodo().withId(3).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 3")).withUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(2).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 2")).withUid(QStringLiteral("uid-2")) + .withParentUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(1).withParent(colId) + .withTitle(QStringLiteral("Item 1")).withUid(QStringLiteral("uid-1")) + .withParentUid(QStringLiteral("uid-2"))); + data.createItem(GenTodo().withId(5).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 5")).withUid(QStringLiteral("uid-5"))); + data.createItem(GenTodo().withId(4).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 4")).withUid(QStringLiteral("uid-4")) + .withParentUid(QStringLiteral("uid-5"))); + + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(1)); + + auto result = queries->findProject(task); + + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 3")); + + // WHEN + data.modifyItem(GenTodo(data.item(1)).withParentUid(QStringLiteral("uid-4"))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 5")); + } + + void findProjectShouldReactToIntermediateParentChange() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + const Akonadi::Collection::Id colId = 42; + data.createCollection(GenCollection().withId(colId).withRootAsParent().withTaskContent()); + + // Three tasks in the collection (two being children of the first one) + // 1->2->3 (project) where 2 changes to 1->2->4 (project) + data.createItem(GenTodo().withId(3).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 3")).withUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(2).withParent(colId) + .withTitle(QStringLiteral("Intermediate item 2")).withUid(QStringLiteral("uid-2")) + .withParentUid(QStringLiteral("uid-3"))); + data.createItem(GenTodo().withId(1).withParent(colId) + .withTitle(QStringLiteral("Item 1")).withUid(QStringLiteral("uid-1")) + .withParentUid(QStringLiteral("uid-2"))); + data.createItem(GenTodo().withId(4).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 4")).withUid(QStringLiteral("uid-4"))); + + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(1)); + + auto result = queries->findProject(task); + + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 3")); + + // WHEN + data.modifyItem(GenTodo(data.item(2)).withParentUid(QStringLiteral("uid-4"))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 4")); + + // AND WHEN + data.removeItem(GenTodo(data.item(2))); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 0); + } + + void findProjectShouldReactToChildItemRemoved() + { + // GIVEN + AkonadiFakeData data; + + // One top level collection + const Akonadi::Collection::Id colId = 42; + data.createCollection(GenCollection().withId(colId).withRootAsParent().withTaskContent()); + + // Three task in the collection: 1->2(project) + data.createItem(GenTodo().withId(2).asProject().withParent(colId) + .withTitle(QStringLiteral("Project 2")).withUid(QStringLiteral("uid-2"))); + data.createItem(GenTodo().withId(1).withParent(colId) + .withTitle(QStringLiteral("Item 1")).withUid(QStringLiteral("uid-1")) + .withParentUid(QStringLiteral("uid-2"))); + + auto serializer = Akonadi::Serializer::Ptr(new Akonadi::Serializer); + + auto cache = Akonadi::Cache::Ptr::create(serializer, Akonadi::MonitorInterface::Ptr(data.createMonitor())); + auto storage = createCachingStorage(data, cache); + QScopedPointer queries(new Akonadi::TaskQueries(storage, + serializer, + Akonadi::MonitorInterface::Ptr(data.createMonitor()), + cache)); + auto task = serializer->createTaskFromItem(data.item(1)); + auto result = queries->findProject(task); + QVERIFY(result); + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 1); + QCOMPARE(result->data().at(0)->name(), QStringLiteral("Project 2")); + + // WHEN + data.removeItem(Akonadi::Item(1)); + + // THEN + TestHelpers::waitForEmptyJobQueue(); + QCOMPARE(result->data().size(), 0); + } + }; ZANSHIN_TEST_MAIN(AkonadiTaskQueriesTest) #include "akonaditaskqueriestest.moc" diff --git a/tests/units/domain/CMakeLists.txt b/tests/units/domain/CMakeLists.txt index 3b4d23cc..2a98fa7b 100644 --- a/tests/units/domain/CMakeLists.txt +++ b/tests/units/domain/CMakeLists.txt @@ -1,12 +1,13 @@ zanshin_auto_tests( artifacttest contexttest datasourcetest livequerytest + liverelationshipquerytest mockitotest notetest projecttest queryresulttest tagtest tasktest ) diff --git a/tests/units/domain/liverelationshipquerytest.cpp b/tests/units/domain/liverelationshipquerytest.cpp new file mode 100644 index 00000000..34011ab3 --- /dev/null +++ b/tests/units/domain/liverelationshipquerytest.cpp @@ -0,0 +1,483 @@ +/* This file is part of Zanshin + + Copyright 2014 Kevin Ottens + Copyright 2018 David Faure + + 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 "domain/livequery.h" + +#include "utils/jobhandler.h" + +#include "testlib/fakejob.h" + +using namespace Domain; + +typedef QSharedPointer QObjectPtr; +static const char objectIdPropName[] = "objectId"; + +class LiveRelationshipQueryTest : public QObject +{ + Q_OBJECT +private: + + QObject *createObject(int id, const QString &name) + { + QObject *obj = new QObject(this); + obj->setObjectName(name); + obj->setProperty(objectIdPropName, id); + return obj; + } + + static bool compareObjectIds(QObject *obj1, QObject *obj2) + { + return obj1->property(objectIdPropName).toInt() == obj2->property(objectIdPropName).toInt(); + } + + static bool isProject(QObject *obj) + { + return obj->objectName().startsWith(QLatin1String("Project")); + } + + static QPair convertToPair(QObject *object) + { + return qMakePair(object->property(objectIdPropName).toInt(), object->objectName()); + } + + static bool representsPair(QObject *object, const QPair &output) { + return object->property(objectIdPropName).toInt() == output.first; + }; + +private slots: + void shouldHaveInitialFetchFunctionAndPredicate() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this] (const Domain::LiveQueryInput::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + add(createObject(3, QStringLiteral("ProjectB"))); + add(createObject(4, QStringLiteral("ItemB"))); + add(createObject(5, QStringLiteral("ParentB"))); + add(createObject(6, QStringLiteral("ProjectC"))); + add(createObject(7, QStringLiteral("ItemC"))); + add(createObject(8, QStringLiteral("ParentC"))); + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + + // WHEN + Domain::QueryResult>::Ptr result = query.result(); + result->data(); + result = query.result(); // Should not cause any problem or wrong data + QVERIFY(result->data().isEmpty()); + + // THEN + QList> expected; + expected << QPair(0, QStringLiteral("ProjectA")) + << QPair(3, QStringLiteral("ProjectB")) + << QPair(6, QStringLiteral("ProjectC")); + QTRY_COMPARE(result->data(), expected); + } + + void shouldFilterOutNullRawPointers() + { + // GIVEN + auto query = Domain::LiveRelationshipQuery(); + query.setFetchFunction([this] (const Domain::LiveQueryInput::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(QStringLiteral("0")); + add(QStringLiteral("1")); + add(QString()); + add(QStringLiteral("a")); + add(QStringLiteral("2")); + }); + }); + query.setConvertFunction([this] (const QString &s) -> QObject* { + bool ok = false; + const int id = s.toInt(&ok); + if (ok) { + auto object = new QObject(this); + object->setProperty("id", id); + return object; + } else { + return Q_NULLPTR; + } + }); + query.setPredicateFunction([] (const QString &s) { + return !s.isEmpty(); + }); + + // WHEN + auto result = query.result(); + result->data(); + result = query.result(); // Should not cause any problem or wrong data + QVERIFY(result->data().isEmpty()); + + // THEN + QTRY_COMPARE(result->data().size(), 3); + QCOMPARE(result->data().at(0)->property("id").toInt(), 0); + QCOMPARE(result->data().at(1)->property("id").toInt(), 1); + QCOMPARE(result->data().at(2)->property("id").toInt(), 2); + } + + void shouldFilterOutNullSharedPointers() + { + // GIVEN + auto query = Domain::LiveRelationshipQuery(); + query.setFetchFunction([this] (const Domain::LiveQueryInput::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(QStringLiteral("0")); + add(QStringLiteral("1")); + add(QString()); + add(QStringLiteral("a")); + add(QStringLiteral("2")); + }); + }); + query.setConvertFunction([this] (const QString &s) { + bool ok = false; + const int id = s.toInt(&ok); + if (ok) { + auto object = QObjectPtr::create(); + object->setProperty("id", id); + return object; + } else { + return QObjectPtr(); + } + }); + query.setPredicateFunction([] (const QString &s) { + return !s.isEmpty(); + }); + + // WHEN + auto result = query.result(); + result->data(); + result = query.result(); // Should not cause any problem or wrong data + QVERIFY(result->data().isEmpty()); + + // THEN + QTRY_COMPARE(result->data().size(), 3); + QCOMPARE(result->data().at(0)->property("id").toInt(), 0); + QCOMPARE(result->data().at(1)->property("id").toInt(), 1); + QCOMPARE(result->data().at(2)->property("id").toInt(), 2); + } + + void shouldDealWithSeveralFetchesProperly() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + add(createObject(3, QStringLiteral("ProjectB"))); + add(createObject(4, QStringLiteral("ItemB"))); + add(createObject(5, QStringLiteral("ParentB"))); + add(createObject(6, QStringLiteral("ProjectC"))); + add(createObject(7, QStringLiteral("ItemC"))); + add(createObject(8, QStringLiteral("ParentC"))); + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + + for (int i = 0; i < 2; i++) { + // WHEN * 2 + Domain::QueryResult>::Ptr result = query.result(); + + // THEN * 2 + QVERIFY(result->data().isEmpty()); + QList> expected; + expected << QPair(0, QStringLiteral("ProjectA")) + << QPair(3, QStringLiteral("ProjectB")) + << QPair(6, QStringLiteral("ProjectC")); + QTRY_COMPARE(result->data(), expected); + } + } + + void shouldClearProviderWhenDeleted() + { + // GIVEN + auto query = new Domain::LiveRelationshipQuery>; + query->setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + }); + }); + query->setConvertFunction(convertToPair); + query->setPredicateFunction(isProject); + query->setCompareFunction(compareObjectIds); + + Domain::QueryResult>::Ptr result = query->result(); + QTRY_COMPARE(result->data().count(), 1); + + // WHEN + delete query; + + // THEN + QVERIFY(result->data().isEmpty()); + } + + void shouldReactToAdds() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + + Domain::QueryResult>::Ptr result = query.result(); + QList> expected{ qMakePair(0, QString::fromLatin1("ProjectA")) }; + QTRY_COMPARE(result->data(), expected); + + // WHEN + query.onAdded(createObject(3, QStringLiteral("ProjectB"))); + query.onAdded(createObject(4, QStringLiteral("ItemB"))); + query.onAdded(createObject(5, QStringLiteral("ParentB"))); + + // THEN + expected << QPair(3, QStringLiteral("ProjectB")); + QCOMPARE(result->data(), expected); + } + + void shouldReactToRemoves() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + query.setRepresentsFunction(representsPair); + + Domain::QueryResult>::Ptr result = query.result(); + QList> expected{ qMakePair(0, QString::fromLatin1("ProjectA")) }; + QTRY_COMPARE(result->data(), expected); + + // WHEN + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] {}); + }); + + // unrelated remove -> ignore + query.onRemoved(createObject(3, QStringLiteral("ItemB"))); + QTRY_COMPARE(result->data(), expected); + + // remove item -> reset happens + query.onRemoved(createObject(1, QStringLiteral("ItemA"))); + + // THEN + expected.clear(); + QTRY_COMPARE(result->data(), expected); + } + + void shouldReactToChanges() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + add(createObject(3, QStringLiteral("ProjectB"))); + add(createObject(4, QStringLiteral("ItemB"))); + add(createObject(5, QStringLiteral("ParentB"))); + add(createObject(6, QStringLiteral("ProjectC"))); + add(createObject(7, QStringLiteral("ItemC"))); + add(createObject(8, QStringLiteral("ParentC"))); + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + query.setRepresentsFunction(representsPair); + + Domain::QueryResult>::Ptr result = query.result(); + QList> expected{ qMakePair(0, QString::fromLatin1("ProjectA")), + qMakePair(3, QString::fromLatin1("ProjectB")), + qMakePair(6, QString::fromLatin1("ProjectC")) }; + QTRY_COMPARE(result->data(), expected); + + // WHEN + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + add(createObject(3, QStringLiteral("ProjectB-Renamed"))); + add(createObject(4, QStringLiteral("ItemB"))); + add(createObject(5, QStringLiteral("ParentB"))); + add(createObject(6, QStringLiteral("ProjectC"))); + add(createObject(7, QStringLiteral("ItemC"))); + add(createObject(8, QStringLiteral("ParentC"))); + }); + }); + query.onChanged(createObject(3, QStringLiteral("whatever"))); + + // THEN + expected[1] = qMakePair(3, QString::fromLatin1("ProjectB-Renamed")); + QTRY_COMPARE(result->data(), expected); + } + + void shouldIgnoreUnrelatedChangesWhenEmpty() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + bool listingDone = false; + query.setFetchFunction([this, &listingDone] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Q_UNUSED(add); + Utils::JobHandler::install(new FakeJob, [&listingDone] { + listingDone = true; + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + query.setRepresentsFunction(representsPair); + + Domain::QueryResult>::Ptr result = query.result(); + QTRY_VERIFY(listingDone); + listingDone = false; + QVERIFY(result->data().isEmpty()); + + // WHEN + query.onChanged(createObject(1, QStringLiteral("ProjectA"))); + + // THEN + QTest::qWait(150); + QVERIFY(!listingDone); + QVERIFY(result->data().isEmpty()); + } + + void shouldAddWhenChangesMakeInputSuitableForQuery() + { + // GIVEN + Domain::LiveRelationshipQuery> query; + bool listingDone = false; + query.setFetchFunction([this, &listingDone] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add, &listingDone] { + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + listingDone = true; + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction(isProject); + query.setCompareFunction(compareObjectIds); + query.setRepresentsFunction(representsPair); + + Domain::QueryResult>::Ptr result = query.result(); + QList> expected; + QTRY_VERIFY(listingDone); + QCOMPARE(result->data(), expected); + + // WHEN + query.setFetchFunction([this] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, add] { + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ProjectA"))); // parent promoted to project + }); + }); + query.onChanged(createObject(2, QStringLiteral("whatever"))); + + // Then + expected << qMakePair(2, QStringLiteral("ProjectA")); + QTRY_COMPARE(result->data(), expected); + } + + void shouldEmptyAndFetchAgainOnReset() + { + // GIVEN + bool afterReset = false; + + Domain::LiveRelationshipQuery> query; + query.setFetchFunction([this, &afterReset] (const Domain::LiveRelationshipQuery::AddFunction &add) { + Utils::JobHandler::install(new FakeJob, [this, &afterReset, add] { + add(createObject(0, QStringLiteral("ProjectA"))); + add(createObject(1, QStringLiteral("ItemA"))); + add(createObject(2, QStringLiteral("ParentA"))); + add(createObject(3, QStringLiteral("ProjectB"))); + add(createObject(4, QStringLiteral("ItemB"))); + add(createObject(5, QStringLiteral("ParentB"))); + + if (afterReset) { + add(createObject(6, QStringLiteral("ProjectC"))); + add(createObject(7, QStringLiteral("ItemC"))); + add(createObject(8, QStringLiteral("ParentC"))); + } + }); + }); + query.setConvertFunction(convertToPair); + query.setPredicateFunction([&afterReset] (QObject *object) { + if (afterReset) + return object->objectName().startsWith(QLatin1String("Item")); + else + return object->objectName().startsWith(QLatin1String("Project")); + }); + query.setCompareFunction(compareObjectIds); + + Domain::QueryResult>::Ptr result = query.result(); + int removeHandlerCallCount = 0; + result->addPostRemoveHandler([&removeHandlerCallCount](const QPair &, int) { + removeHandlerCallCount++; + }); + + QTRY_VERIFY(!result->data().isEmpty()); + QCOMPARE(removeHandlerCallCount, 0); + + // WHEN + query.reset(); + afterReset = true; + + // THEN + const QList> expected = { qMakePair(1, QStringLiteral("ItemA")), + qMakePair(4, QStringLiteral("ItemB")), + qMakePair(7, QStringLiteral("ItemC")) }; + QTRY_COMPARE(result->data(), expected); + QCOMPARE(removeHandlerCallCount, 2); + } +}; + +ZANSHIN_TEST_MAIN(LiveRelationshipQueryTest) + +#include "liverelationshipquerytest.moc" diff --git a/tests/units/presentation/workdaypagemodeltest.cpp b/tests/units/presentation/workdaypagemodeltest.cpp index 4fbe246b..31bee2b1 100644 --- a/tests/units/presentation/workdaypagemodeltest.cpp +++ b/tests/units/presentation/workdaypagemodeltest.cpp @@ -1,379 +1,392 @@ /* This file is part of Zanshin Copyright 2015 Theo Vaucher 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 #include #include #include "utils/datetime.h" #include "utils/mockobject.h" #include "domain/noterepository.h" #include "domain/taskqueries.h" #include "domain/taskrepository.h" #include "presentation/workdaypagemodel.h" using namespace mockitopp; using namespace mockitopp::matcher; class WorkdayPageModelTest : public QObject { Q_OBJECT private slots: void shouldListWorkdayInCentralListModel() { // GIVEN const auto today = Utils::DateTime::currentDate(); // Three tasks auto task1 = Domain::Task::Ptr::create(); task1->setTitle(QStringLiteral("task1")); task1->setStartDate(today.addDays(-10)); task1->setDueDate(today); auto task2 = Domain::Task::Ptr::create(); task2->setTitle(QStringLiteral("task2")); task2->setStartDate(today); task2->setDueDate(today.addDays(10)); auto task3 = Domain::Task::Ptr::create(); task3->setTitle(QStringLiteral("task3")); task3->setStartDate(today.addYears(-4)); task3->setDueDate(today.addYears(-3)); auto taskProvider = Domain::QueryResultProvider::Ptr::create(); auto taskResult = Domain::QueryResult::create(taskProvider); taskProvider->append(task1); taskProvider->append(task2); taskProvider->append(task3); // Two tasks under the task1 auto childTask11 = Domain::Task::Ptr::create(); childTask11->setTitle(QStringLiteral("childTask11")); auto childTask12 = Domain::Task::Ptr::create(); childTask12->setTitle(QStringLiteral("childTask12")); childTask12->setStartDate(today); childTask12->setDueDate(today); auto childTaskProvider = Domain::QueryResultProvider::Ptr::create(); auto childTaskResult = Domain::QueryResult::create(childTaskProvider); taskProvider->append(childTask12); childTaskProvider->append(childTask11); childTaskProvider->append(childTask12); + // One project + auto project = Domain::Project::Ptr::create(); + project->setName("KDE"); + auto projectProvider = Domain::QueryResultProvider::Ptr::create(); + auto projectResult = Domain::QueryResult::create(projectProvider); + projectProvider->append(project); + Utils::MockObject taskQueriesMock; taskQueriesMock(&Domain::TaskQueries::findWorkdayTopLevel).when().thenReturn(taskResult); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task1).thenReturn(childTaskResult); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task2).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task3).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(childTask11).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(childTask12).thenReturn(Domain::QueryResult::Ptr()); + taskQueriesMock(&Domain::TaskQueries::findProject).when(task1).thenReturn(Domain::QueryResult::Ptr()); + taskQueriesMock(&Domain::TaskQueries::findProject).when(task2).thenReturn(projectResult); + Utils::MockObject taskRepositoryMock; Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), taskRepositoryMock.getInstance()); // WHEN QAbstractItemModel *model = workday.centralListModel(); // THEN const QModelIndex task1Index = model->index(0, 0); const QModelIndex task2Index = model->index(1, 0); const QModelIndex task3Index = model->index(2, 0); const QModelIndex taskChildTask12Index = model->index(3, 0); const QModelIndex childTask11Index = model->index(0, 0, task1Index); const QModelIndex childTask12Index = model->index(1, 0, task1Index); QCOMPARE(model->rowCount(), 4); QCOMPARE(model->rowCount(task1Index), 2); QCOMPARE(model->rowCount(task2Index), 0); QCOMPARE(model->rowCount(task3Index), 0); QCOMPARE(model->rowCount(taskChildTask12Index), 0); QVERIFY(childTask11Index.isValid()); QVERIFY(childTask12Index.isValid()); QCOMPARE(model->rowCount(childTask11Index), 0); QCOMPARE(model->rowCount(childTask12Index), 0); const Qt::ItemFlags defaultFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; QCOMPARE(model->flags(task1Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->flags(childTask11Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->flags(childTask12Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->flags(task2Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->flags(task3Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->flags(taskChildTask12Index), defaultFlags | Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled); QCOMPARE(model->data(task1Index).toString(), task1->title()); QCOMPARE(model->data(childTask11Index).toString(), childTask11->title()); QCOMPARE(model->data(childTask12Index).toString(), childTask12->title()); QCOMPARE(model->data(task2Index).toString(), task2->title()); QCOMPARE(model->data(task3Index).toString(), task3->title()); QCOMPARE(model->data(taskChildTask12Index).toString(), childTask12->title()); QCOMPARE(model->data(task1Index, Qt::EditRole).toString(), task1->title()); QCOMPARE(model->data(childTask11Index, Qt::EditRole).toString(), childTask11->title()); QCOMPARE(model->data(childTask12Index, Qt::EditRole).toString(), childTask12->title()); QCOMPARE(model->data(task2Index, Qt::EditRole).toString(), task2->title()); QCOMPARE(model->data(task3Index, Qt::EditRole).toString(), task3->title()); QCOMPARE(model->data(taskChildTask12Index, Qt::EditRole).toString(), childTask12->title()); QVERIFY(model->data(task1Index, Qt::CheckStateRole).isValid()); QVERIFY(model->data(childTask11Index, Qt::CheckStateRole).isValid()); QVERIFY(model->data(childTask12Index, Qt::CheckStateRole).isValid()); QVERIFY(model->data(task2Index, Qt::CheckStateRole).isValid()); QVERIFY(model->data(task3Index, Qt::CheckStateRole).isValid()); QVERIFY(model->data(taskChildTask12Index, Qt::CheckStateRole).isValid()); QCOMPARE(model->data(task1Index, Qt::CheckStateRole).toBool(), task1->isDone()); QCOMPARE(model->data(childTask11Index, Qt::CheckStateRole).toBool(), childTask11->isDone()); QCOMPARE(model->data(childTask12Index, Qt::CheckStateRole).toBool(), childTask12->isDone()); QCOMPARE(model->data(task2Index, Qt::CheckStateRole).toBool(), task2->isDone()); QCOMPARE(model->data(task3Index, Qt::CheckStateRole).toBool(), task3->isDone()); QCOMPARE(model->data(taskChildTask12Index, Qt::CheckStateRole).toBool(), childTask12->isDone()); + QCOMPARE(model->data(task1Index, Presentation::WorkdayPageModel::ProjectRole).toString(), "Inbox"); + QCOMPARE(model->data(task2Index, Presentation::WorkdayPageModel::ProjectRole).toString(), "Project: KDE"); + // WHEN taskRepositoryMock(&Domain::TaskRepository::update).when(task1).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::update).when(childTask11).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::update).when(childTask12).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::update).when(task2).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::update).when(task3).thenReturn(new FakeJob(this)); QVERIFY(model->setData(task1Index, "newTask1")); QVERIFY(model->setData(childTask11Index, "newChildTask11")); QVERIFY(model->setData(task2Index, "newTask2")); QVERIFY(model->setData(task3Index, "newTask3")); QVERIFY(model->setData(taskChildTask12Index, "newChildTask12")); QVERIFY(model->setData(task1Index, Qt::Unchecked, Qt::CheckStateRole)); QVERIFY(model->setData(childTask11Index, Qt::Unchecked, Qt::CheckStateRole)); QVERIFY(model->setData(task2Index, Qt::Checked, Qt::CheckStateRole)); QVERIFY(model->setData(task3Index, Qt::Unchecked, Qt::CheckStateRole)); QVERIFY(model->setData(taskChildTask12Index, Qt::Checked, Qt::CheckStateRole)); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(task1).exactly(2)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(childTask11).exactly(2)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(childTask12).exactly(2)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(task2).exactly(2)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(task3).exactly(2)); QCOMPARE(task1->title(), QStringLiteral("newTask1")); QCOMPARE(childTask11->title(), QStringLiteral("newChildTask11")); QCOMPARE(childTask12->title(), QStringLiteral("newChildTask12")); QCOMPARE(task2->title(), QStringLiteral("newTask2")); QCOMPARE(task3->title(), QStringLiteral("newTask3")); QCOMPARE(task1->isDone(), false); QCOMPARE(childTask11->isDone(), false); QCOMPARE(childTask12->isDone(), true); QCOMPARE(task2->isDone(), true); QCOMPARE(task3->isDone(), false); // WHEN auto data = std::unique_ptr(model->mimeData(QModelIndexList() << childTask12Index)); // THEN QVERIFY(data->hasFormat(QStringLiteral("application/x-zanshin-object"))); QCOMPARE(data->property("objects").value(), Domain::Artifact::List() << childTask12); // WHEN auto childTask2 = Domain::Task::Ptr::create(); taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask11, childTask2).thenReturn(new FakeJob(this)); data.reset(new QMimeData); data->setData(QStringLiteral("application/x-zanshin-object"), "object"); data->setProperty("objects", QVariant::fromValue(Domain::Artifact::List() << childTask2)); model->dropMimeData(data.get(), Qt::MoveAction, -1, -1, childTask11Index); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask11, childTask2).exactly(1)); // WHEN auto childTask3 = Domain::Task::Ptr::create(); auto childTask4 = Domain::Task::Ptr::create(); taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask12, childTask3).thenReturn(new FakeJob(this)); taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask12, childTask4).thenReturn(new FakeJob(this)); data.reset(new QMimeData); data->setData(QStringLiteral("application/x-zanshin-object"), "object"); data->setProperty("objects", QVariant::fromValue(Domain::Artifact::List() << childTask3 << childTask4)); model->dropMimeData(data.get(), Qt::MoveAction, -1, -1, childTask12Index); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask12, childTask3).exactly(1)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::associate).when(childTask12, childTask4).exactly(1)); // WHEN auto childTask5 = Domain::Task::Ptr::create(); QVERIFY(!childTask5->startDate().isValid()); taskRepositoryMock(&Domain::TaskRepository::dissociate).when(childTask5).thenReturn(new FakeJob(this)); data.reset(new QMimeData); data->setData(QStringLiteral("application/x-zanshin-object"), "object"); data->setProperty("objects", QVariant::fromValue(Domain::Artifact::List() << childTask5)); model->dropMimeData(data.get(), Qt::MoveAction, -1, -1, QModelIndex()); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::dissociate).when(childTask5).exactly(1)); QVERIFY(taskRepositoryMock(&Domain::TaskRepository::update).when(childTask5).exactly(0)); QCOMPARE(childTask5->startDate(), today); } void shouldAddTasksInWorkdayPage() { // GIVEN // ... in fact we won't list any model Utils::MockObject taskQueriesMock; // We'll gladly create a task though Utils::MockObject taskRepositoryMock; taskRepositoryMock(&Domain::TaskRepository::create).when(any()).thenReturn(new FakeJob(this)); Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), taskRepositoryMock.getInstance()); // WHEN auto title = QStringLiteral("New task"); auto today = Utils::DateTime::currentDate(); auto task = workday.addItem(title).objectCast(); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::create).when(any()).exactly(1)); QVERIFY(task); QCOMPARE(task->title(), title); QCOMPARE(task->startDate(), today); } void shouldAddChildTask() { // GIVEN // Two tasks auto task1 = Domain::Task::Ptr::create(); auto task2 = Domain::Task::Ptr::create(); auto taskProvider = Domain::QueryResultProvider::Ptr::create(); auto taskResult = Domain::QueryResult::create(taskProvider); taskProvider->append(task1); taskProvider->append(task2); Utils::MockObject taskQueriesMock; taskQueriesMock(&Domain::TaskQueries::findWorkdayTopLevel).when().thenReturn(taskResult); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task1).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task2).thenReturn(Domain::QueryResult::Ptr()); Utils::MockObject taskRepositoryMock; taskRepositoryMock(&Domain::TaskRepository::createChild).when(any(), any()) .thenReturn(new FakeJob(this)); Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), taskRepositoryMock.getInstance()); // WHEN const auto title = QStringLiteral("New task"); const auto parentIndex = workday.centralListModel()->index(0, 0); const auto createdTask = workday.addItem(title, parentIndex).objectCast(); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::createChild).when(any(), any()) .exactly(1)); QVERIFY(createdTask); QCOMPARE(createdTask->title(), title); QVERIFY(!createdTask->startDate().isValid()); } void shouldDeleteItems() { // GIVEN // Two tasks auto task1 = Domain::Task::Ptr::create(); auto task2 = Domain::Task::Ptr::create(); auto taskProvider = Domain::QueryResultProvider::Ptr::create(); auto taskResult = Domain::QueryResult::create(taskProvider); taskProvider->append(task1); taskProvider->append(task2); Utils::MockObject taskQueriesMock; taskQueriesMock(&Domain::TaskQueries::findWorkdayTopLevel).when().thenReturn(taskResult); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task1).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task2).thenReturn(Domain::QueryResult::Ptr()); Utils::MockObject taskRepositoryMock; taskRepositoryMock(&Domain::TaskRepository::remove).when(task2).thenReturn(new FakeJob(this)); Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), taskRepositoryMock.getInstance()); // WHEN const QModelIndex index = workday.centralListModel()->index(1, 0); workday.removeItem(index); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::remove).when(task2).exactly(1)); } void shouldPromoteItem() { // GIVEN // Two tasks auto task1 = Domain::Task::Ptr::create(); auto task2 = Domain::Task::Ptr::create(); auto taskProvider = Domain::QueryResultProvider::Ptr::create(); auto taskResult = Domain::QueryResult::create(taskProvider); taskProvider->append(task1); taskProvider->append(task2); Utils::MockObject taskQueriesMock; taskQueriesMock(&Domain::TaskQueries::findWorkdayTopLevel).when().thenReturn(taskResult); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task1).thenReturn(Domain::QueryResult::Ptr()); taskQueriesMock(&Domain::TaskQueries::findChildren).when(task2).thenReturn(Domain::QueryResult::Ptr()); Utils::MockObject taskRepositoryMock; taskRepositoryMock(&Domain::TaskRepository::promoteToProject).when(task2).thenReturn(new FakeJob(this)); Presentation::WorkdayPageModel workday(taskQueriesMock.getInstance(), taskRepositoryMock.getInstance()); // WHEN const QModelIndex index = workday.centralListModel()->index(1, 0); workday.promoteItem(index); // THEN QVERIFY(taskRepositoryMock(&Domain::TaskRepository::promoteToProject).when(task2).exactly(1)); } }; ZANSHIN_TEST_MAIN(WorkdayPageModelTest) #include "workdaypagemodeltest.moc"