File indexing completed on 2025-01-19 04:52:01

0001 /*
0002     Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
0003 
0004     This library is free software; you can redistribute it and/or modify it
0005     under the terms of the GNU Library General Public License as published by
0006     the Free Software Foundation; either version 2 of the License, or (at your
0007     option) any later version.
0008 
0009     This library is distributed in the hope that it will be useful, but WITHOUT
0010     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0011     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0012     License for more details.
0013 
0014     You should have received a copy of the GNU Library General Public License
0015     along with this library; see the file COPYING.LIB.  If not, write to the
0016     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0017     02110-1301, USA.
0018 */
0019 
0020 #include "todomodel.h"
0021 
0022 #include <sink/log.h>
0023 #include <sink/query.h>
0024 #include <sink/store.h>
0025 #include <sink/applicationdomaintype.h>
0026 
0027 #include <QMetaEnum>
0028 
0029 #include <KCalendarCore/ICalFormat>
0030 
0031 #include <entitycache.h>
0032 
0033 using namespace Sink;
0034 
0035 TodoSourceModel::TodoSourceModel(QObject *parent)
0036     : QAbstractItemModel(parent),
0037     mCalendarCache{EntityCache<ApplicationDomain::Calendar>::Ptr::create(QByteArrayList{{ApplicationDomain::Calendar::Color::name}, {ApplicationDomain::Calendar::Name::name}})},
0038     mUpdateFromSourceDebouncer{100, [this] { this->updateFromSource(); }, this}
0039 {
0040 }
0041 
0042 static QList<QByteArray> toList(const QVariant &variant) {
0043     if (variant.type() == static_cast<QVariant::Type>(QMetaType::QVariantList)) {
0044         QList<QByteArray> list;
0045         for (const auto &v : variant.toList()) {
0046             list.append(v.toByteArray());
0047         }
0048         return list;
0049     }
0050     return variant.value<QSet<QByteArray>>().values();
0051 }
0052 
0053 void TodoSourceModel::setFilter(const QVariantMap &filter)
0054 {
0055     const auto account = filter.value("account").toByteArray();
0056     const auto calendarFilter = toList(filter.value("calendars"));
0057     using namespace Sink::ApplicationDomain;
0058     if (calendarFilter.isEmpty()) {
0059         refreshView();
0060         return;
0061     }
0062 
0063     Sink::Query query;
0064     if (!account.isEmpty()) {
0065         query.resourceFilter<SinkResource::Account>(account);
0066     }
0067     query.filter<Todo::Calendar>(Query::Comparator(QVariant::fromValue(calendarFilter), Query::Comparator::In));
0068     query.filter<Todo::ParentUid>(filter.value("parentUid"));
0069 
0070     if (filter.value("doing").toBool()) {
0071         query.filter<Todo::Status>("INPROCESS");
0072     } else if (filter.value("inbox").toBool()) {
0073         query.filter<Todo::Status>(Query::Comparator{QVariant::fromValue(QByteArrayList{"NEEDSACTION", ""}), Query::Comparator::In});
0074     }
0075 
0076     query.setFlags(Sink::Query::LiveQuery);
0077     query.request<Todo::Summary>();
0078     query.request<Todo::Description>();
0079     query.request<Todo::StartDate>();
0080     query.request<Todo::DueDate>();
0081     query.request<Todo::CompletedDate>();
0082     query.request<Todo::Status>();
0083     query.request<Todo::Calendar>();
0084     query.request<Todo::Ical>();
0085     query.request<Todo::Priority>();
0086 
0087     mSourceModel = Store::loadModel<ApplicationDomain::Todo>(query);
0088 
0089     QObject::connect(mSourceModel.data(), &QAbstractItemModel::dataChanged, this, &TodoSourceModel::refreshView);
0090     QObject::connect(mSourceModel.data(), &QAbstractItemModel::layoutChanged, this, &TodoSourceModel::refreshView);
0091     QObject::connect(mSourceModel.data(), &QAbstractItemModel::modelReset, this, &TodoSourceModel::refreshView);
0092     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsInserted, this, &TodoSourceModel::refreshView);
0093     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsMoved, this, &TodoSourceModel::refreshView);
0094     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &TodoSourceModel::refreshView);
0095 
0096     refreshView();
0097 }
0098 
0099 void TodoSourceModel::refreshView()
0100 {
0101     mUpdateFromSourceDebouncer.trigger();
0102 }
0103 
0104 void TodoSourceModel::updateFromSource()
0105 {
0106     beginResetModel();
0107 
0108     mTodos.clear();
0109 
0110     if (mSourceModel) {
0111         for (int i = 0; i < mSourceModel->rowCount(); ++i) {
0112             auto todo = mSourceModel->index(i, 0).data(Sink::Store::DomainObjectRole).value<ApplicationDomain::Todo::Ptr>();
0113             //Parse the todo
0114             if(auto icalTodo = KCalendarCore::ICalFormat().readIncidence(todo->getIcal()).dynamicCast<KCalendarCore::Todo>()) {
0115                 mTodos.append({icalTodo->dtStart(), icalTodo->dtDue(), icalTodo->completed(), icalTodo, getColor(todo->getCalendar()), getCalendarName(todo->getCalendar()), todo->getStatus(), todo, todo->getPriority()});
0116             } else {
0117                 SinkWarning() << "Invalid ICal to process, ignoring...";
0118             }
0119         }
0120     }
0121 
0122     endResetModel();
0123 }
0124 
0125 QModelIndex TodoSourceModel::index(int row, int column, const QModelIndex &parent) const
0126 {
0127     if (!hasIndex(row, column, parent)) {
0128         return {};
0129     }
0130 
0131     if (!parent.isValid()) {
0132         return createIndex(row, column);
0133     }
0134     return {};
0135 }
0136 
0137 QModelIndex TodoSourceModel::parent(const QModelIndex &) const
0138 {
0139     return {};
0140 }
0141 
0142 int TodoSourceModel::rowCount(const QModelIndex &parent) const
0143 {
0144     if (!parent.isValid()) {
0145         return mTodos.size();
0146     }
0147     return 0;
0148 }
0149 
0150 int TodoSourceModel::columnCount(const QModelIndex &) const
0151 {
0152     return 1;
0153 }
0154 
0155 QByteArray TodoSourceModel::getColor(const QByteArray &calendar) const
0156 {
0157     const auto color = mCalendarCache->getProperty(calendar, {ApplicationDomain::Calendar::Color::name}).toByteArray();
0158     if (color.isEmpty()) {
0159         qWarning() << "Failed to get color for calendar " << calendar;
0160     }
0161     return color;
0162 }
0163 
0164 QString TodoSourceModel::getCalendarName(const QByteArray &calendar) const
0165 {
0166     const auto name = mCalendarCache->getProperty(calendar, {ApplicationDomain::Calendar::Name::name}).toString();
0167     if (name.isEmpty()) {
0168         qWarning() << "Failed to get name for calendar " << calendar;
0169     }
0170     return name;
0171 }
0172 
0173 QVariant TodoSourceModel::data(const QModelIndex &idx, int role) const
0174 {
0175     if (!hasIndex(idx.row(), idx.column())) {
0176         return {};
0177     }
0178     auto todo = mTodos.at(idx.row());
0179     auto icalTodo = todo.incidence;
0180     switch (role) {
0181         case Summary:
0182             return icalTodo->summary();
0183         case Description:
0184             return icalTodo->description();
0185         case StartDate:
0186             return todo.start;
0187         case DueDate:
0188             return todo.due;
0189         case Date:
0190             if (todo.status == "COMPLETED") {
0191                 return todo.completed;
0192             }
0193             if (todo.due.isValid()) {
0194                 return todo.due;
0195             }
0196             return todo.start;
0197         case CompletedDate:
0198             return todo.completed;
0199         case Color:
0200             return todo.color;
0201         case Calendar:
0202             return todo.calendarName;
0203         case Status:
0204             return todo.status;
0205         case Complete:
0206             return todo.status == "COMPLETED";
0207         case Doing:
0208             return todo.status == "INPROCESS";
0209         case Important:
0210             return todo.priority == 1;
0211         case Relevance: {
0212             int score = 100;
0213             if (todo.status == "COMPLETED") {
0214                 score -= 100;
0215             } else {
0216                 //TODO add if from current account
0217                 //Due
0218                 if (todo.due.isValid() && (!todo.start.isValid() || todo.start > QDateTime::currentDateTime())) {
0219                     score += 10;
0220                     //Overdue
0221                     if (todo.due >= QDateTime::currentDateTime()) {
0222                         score += 50;
0223                     }
0224                 }
0225                 if (todo.priority == 1) {
0226                     score += 50;
0227                 }
0228                 if (todo.status == "INPROCESS") {
0229                     score += 100;
0230                 }
0231             }
0232             return score;
0233         }
0234         case Todo:
0235             return QVariant::fromValue(todo.domainObject);
0236         case SortDate:
0237             if (todo.due.isValid()) {
0238                 return todo.due;
0239             }
0240             if (todo.start.isValid()) {
0241                 return todo.start;
0242             }
0243             return icalTodo->lastModified();
0244         default:
0245             SinkWarning() << "Unknown role for todo:" << QMetaEnum::fromType<Roles>().valueToKey(role) << role;
0246             return {};
0247     }
0248 }
0249 
0250 QHash<int, QByteArray> TodoSourceModel::roleNames() const
0251 {
0252     return {
0253         {Summary, "summary"},
0254         {Description, "description"},
0255         {StartDate, "startDate"},
0256         {DueDate, "dueDate"},
0257         {CompletedDate, "completedDate"},
0258         {Date, "date"},
0259         {Color, "color"},
0260         {Calendar, "calendar"},
0261         {Status, "status"},
0262         {Complete, "complete"},
0263         {Doing, "doing"},
0264         {Important, "important"},
0265         {Todo, "domainObject"}
0266     };
0267 }
0268 
0269 
0270 
0271 TodoModel::TodoModel(QObject *parent)
0272     : QSortFilterProxyModel(parent)
0273 {
0274     setDynamicSortFilter(true);
0275     sort(0, Qt::DescendingOrder);
0276     setFilterCaseSensitivity(Qt::CaseInsensitive);
0277     setFilterRole(TodoSourceModel::Summary);
0278     setSourceModel(new TodoSourceModel(this));
0279 }
0280 
0281 QHash<int, QByteArray> TodoModel::roleNames() const
0282 {
0283     return sourceModel()->roleNames();
0284 }
0285 
0286 void TodoModel::setFilter(const QVariantMap &f)
0287 {
0288     static_cast<TodoSourceModel*>(sourceModel())->setFilter(f);
0289     mFilterNotStarted = f.value("doing").toBool();
0290     const auto filterString = f.value("string").toString();
0291     setFilterFixedString(filterString);
0292 }
0293 
0294 bool TodoModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
0295 {
0296     const auto leftScore = left.data(TodoSourceModel::Relevance).toInt();
0297     const auto rightScore = right.data(TodoSourceModel::Relevance).toInt();
0298     if (leftScore == rightScore) {
0299         const auto leftDate = left.data(TodoSourceModel::SortDate).toDateTime();
0300         const auto rightDate = right.data(TodoSourceModel::SortDate).toDateTime();
0301         if (leftDate == rightDate) {
0302             return left.data(TodoSourceModel::Summary).toString() < right.data(TodoSourceModel::Summary).toString();
0303         }
0304         return leftDate < rightDate;
0305     }
0306     return leftScore < rightScore;
0307 }
0308 
0309 bool TodoModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
0310 {
0311     if (mFilterNotStarted) {
0312         const auto startDate = sourceModel()->index(sourceRow, 0, sourceParent).data(TodoSourceModel::StartDate).value<QDateTime>();
0313         if (startDate.isValid() && startDate < QDateTime::currentDateTime()) {
0314             return false;
0315         }
0316     }
0317     return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
0318 }