File indexing completed on 2025-01-19 04:51:58

0001 /*
0002     Copyright (c) 2018 Michael Bohlender <michael.bohlender@kdemail.net>
0003     Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
0004     Copyright (c) 2018 RĂ©mi Nicole <minijackson@riseup.net>
0005 
0006     This library is free software; you can redistribute it and/or modify it
0007     under the terms of the GNU Library General Public License as published by
0008     the Free Software Foundation; either version 2 of the License, or (at your
0009     option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful, but WITHOUT
0012     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0014     License for more details.
0015 
0016     You should have received a copy of the GNU Library General Public License
0017     along with this library; see the file COPYING.LIB.  If not, write to the
0018     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0019     02110-1301, USA.
0020 */
0021 
0022 #include "eventoccurrencemodel.h"
0023 
0024 #include <sink/log.h>
0025 #include <sink/query.h>
0026 #include <sink/store.h>
0027 #include <sink/applicationdomaintype.h>
0028 
0029 #include <QMetaEnum>
0030 
0031 #include <KCalendarCore/ICalFormat>
0032 #include <KCalendarCore/OccurrenceIterator>
0033 #include <KCalendarCore/MemoryCalendar>
0034 
0035 #include <entitycache.h>
0036 
0037 #include <algorithm>
0038 #include <vector>
0039 #include <iterator>
0040 
0041 using namespace Sink;
0042 
0043 EventOccurrenceModel::EventOccurrenceModel(QObject *parent)
0044     : QAbstractItemModel(parent),
0045     mCalendarCache{EntityCache<ApplicationDomain::Calendar>::Ptr::create(QByteArrayList{{ApplicationDomain::Calendar::Color::name}})},
0046     mUpdateFromSourceDebouncer{100,[this] { this->updateFromSource(); }, this}
0047 {
0048 }
0049 
0050 void EventOccurrenceModel::setStart(const QDate &start)
0051 {
0052     if (start != mStart) {
0053         mStart = start;
0054         updateQuery();
0055     }
0056 }
0057 
0058 QDate EventOccurrenceModel::start() const
0059 {
0060     return mStart;
0061 }
0062 
0063 void EventOccurrenceModel::setLength(int length)
0064 {
0065     mLength = length;
0066     updateQuery();
0067 }
0068 
0069 int EventOccurrenceModel::length() const
0070 {
0071     return mLength;
0072 }
0073 
0074 void EventOccurrenceModel::setCalendarFilter(const QList<QString> &calendarFilter)
0075 {
0076     mCalendarFilter.clear();
0077     for (const auto &id : calendarFilter) {
0078         mCalendarFilter << id.toLatin1();
0079     }
0080     updateQuery();
0081 }
0082 
0083 void EventOccurrenceModel::setFilter(const QVariantMap &filter)
0084 {
0085     mFilter = filter;
0086     updateQuery();
0087 }
0088 
0089 void EventOccurrenceModel::updateQuery()
0090 {
0091     using namespace Sink::ApplicationDomain;
0092     mInitialItemsLoaded = false;
0093     if (mCalendarFilter.isEmpty() || !mLength || !mStart.isValid()) {
0094         mSourceModel.clear();
0095         refreshView();
0096         return;
0097     }
0098     mEnd = mStart.addDays(mLength);
0099 
0100     Sink::Query query;
0101     query.setFlags(Sink::Query::LiveQuery);
0102     query.request<Event::Summary>();
0103     query.request<Event::Description>();
0104     query.request<Event::StartTime>();
0105     query.request<Event::EndTime>();
0106     query.request<Event::Calendar>();
0107     query.request<Event::Ical>();
0108     query.request<Event::AllDay>();
0109 
0110     query.filter<Event::StartTime, Event::EndTime>(Sink::Query::Comparator(QVariantList{mStart, mEnd}, Sink::Query::Comparator::Overlap));
0111 
0112     query.setPostQueryFilter([=, filter = mFilter, calendarFilter = mCalendarFilter] (const ApplicationDomain::ApplicationDomainType &entity){
0113         const Sink::ApplicationDomain::Event event(entity);
0114         if (!calendarFilter.contains(event.getCalendar())) {
0115             return false;
0116         }
0117 
0118         for (auto it = filter.constBegin(); it!= filter.constEnd(); it++) {
0119             if (event.getProperty(it.key().toLatin1()) != it.value()) {
0120                 return false;
0121             }
0122         }
0123 
0124         return true;
0125     });
0126 
0127     mSourceModel = Store::loadModel<ApplicationDomain::Event>(query);
0128 
0129     QObject::connect(mSourceModel.data(), &QAbstractItemModel::dataChanged, this, &EventOccurrenceModel::refreshView);
0130     QObject::connect(mSourceModel.data(), &QAbstractItemModel::layoutChanged, this, &EventOccurrenceModel::refreshView);
0131     QObject::connect(mSourceModel.data(), &QAbstractItemModel::modelReset, this, &EventOccurrenceModel::refreshView);
0132     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsInserted, this, &EventOccurrenceModel::refreshView);
0133     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsMoved, this, &EventOccurrenceModel::refreshView);
0134     QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &EventOccurrenceModel::refreshView);
0135 
0136     refreshView();
0137 }
0138 
0139 void EventOccurrenceModel::refreshView()
0140 {
0141     mUpdateFromSourceDebouncer.trigger();
0142 }
0143 
0144 void EventOccurrenceModel::updateFromSource()
0145 {
0146     QList<Occurrence> newEvents;
0147 
0148     if (mSourceModel) {
0149         QMap<QByteArray, KCalendarCore::Incidence::Ptr> recurringEvents;
0150         QMultiMap<QByteArray, KCalendarCore::Incidence::Ptr> exceptions;
0151         QMap<QByteArray, QSharedPointer<Sink::ApplicationDomain::Event>> events;
0152         for (int i = 0; i < mSourceModel->rowCount(); ++i) {
0153             auto event = mSourceModel->index(i, 0).data(Sink::Store::DomainObjectRole).value<ApplicationDomain::Event::Ptr>();
0154             Q_ASSERT(event);
0155 
0156             //Parse the event
0157             auto icalEvent = KCalendarCore::ICalFormat().readIncidence(event->getIcal()).dynamicCast<KCalendarCore::Event>();
0158             if(!icalEvent) {
0159                 SinkWarning() << "Invalid ICal to process, ignoring...";
0160                 continue;
0161             }
0162 
0163             //Collect recurring events and add the rest immediately
0164             if (icalEvent->recurs()) {
0165                 recurringEvents.insert(icalEvent->uid().toLatin1(), icalEvent);
0166                 events.insert(icalEvent->instanceIdentifier().toLatin1(), event);
0167             } else if(icalEvent->recurrenceId().isValid()) {
0168                 exceptions.insert(icalEvent->uid().toLatin1(), icalEvent);
0169                 events.insert(icalEvent->instanceIdentifier().toLatin1(), event);
0170             } else {
0171                 if (icalEvent->dtStart().date() < mEnd && icalEvent->dtEnd().date() >= mStart) {
0172                     newEvents.append({icalEvent->dtStart(), icalEvent->dtEnd(), icalEvent, getColor(event->getCalendar()), event->getAllDay(), event});
0173                 }
0174             }
0175         }
0176         //process all recurring events and their exceptions.
0177         for (const auto &uid : recurringEvents.keys()) {
0178             KCalendarCore::MemoryCalendar calendar{QTimeZone::systemTimeZone()};
0179             calendar.addIncidence(recurringEvents.value(uid));
0180             for (const auto &event : exceptions.values(uid)) {
0181                 calendar.addIncidence(event);
0182             }
0183             KCalendarCore::OccurrenceIterator occurrenceIterator{calendar, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}};
0184             while (occurrenceIterator.hasNext()) {
0185                 occurrenceIterator.next();
0186                 const auto incidence = occurrenceIterator.incidence();
0187                 Q_ASSERT(incidence);
0188                 const auto event = events.value(incidence->instanceIdentifier().toLatin1());
0189                 const auto start = occurrenceIterator.occurrenceStartDate();
0190                 const auto end = incidence->endDateForStart(start);
0191                 if (start.date() < mEnd && end.date() >= mStart) {
0192                     newEvents.append({start, end, incidence, getColor(event->getCalendar()), event->getAllDay(), event});
0193                 }
0194             }
0195         }
0196         //Process all exceptions that had no main event present in the current query
0197         for (const auto &uid : exceptions.keys()) {
0198             const auto icalEvent = exceptions.value(uid).dynamicCast<KCalendarCore::Event>();
0199             Q_ASSERT(icalEvent);
0200             const auto event = events.value(icalEvent->instanceIdentifier().toLatin1());
0201             if (icalEvent->dtStart().date() < mEnd && icalEvent->dtEnd().date() >= mStart) {
0202                 newEvents.append({icalEvent->dtStart(), icalEvent->dtEnd(), icalEvent, getColor(event->getCalendar()), event->getAllDay(), event});
0203             }
0204         }
0205     }
0206 
0207     {
0208         auto it = std::begin(mEvents);
0209         while (it != std::end(mEvents)) {
0210             const auto event = *it;
0211             auto itToRemove = std::find_if(std::begin(newEvents), std::end(newEvents), [&] (const auto &e) {
0212                 Q_ASSERT(e.incidence);
0213                 Q_ASSERT(event.incidence);
0214                 return e.incidence->uid() == event.incidence->uid() && e.start == event.start;
0215             });
0216             // Can't find the vevent in newEvents anymore, so remove from list
0217             if (itToRemove == std::end(newEvents)) {
0218                 //Removed
0219                 const int startIndex = std::distance(std::begin(mEvents), it);
0220                 beginRemoveRows(QModelIndex(), startIndex, startIndex);
0221                 it = mEvents.erase(it);
0222                 endRemoveRows();
0223             } else {
0224                 it++;
0225             }
0226         }
0227     }
0228 
0229     for (auto newIt = std::begin(newEvents); newIt != std::end(newEvents); newIt++) {
0230         const auto event = *newIt;
0231         auto it = std::find_if(std::begin(mEvents), std::end(mEvents), [&] (const auto &e) {
0232             Q_ASSERT(e.incidence);
0233             return e.incidence->uid() == event.incidence->uid() && e.start == event.start;
0234         });
0235         if (it == std::end(mEvents)) {
0236             //New event
0237             const int startIndex = std::distance(std::begin(newEvents), newIt);
0238             beginInsertRows(QModelIndex(), startIndex, startIndex);
0239             mEvents.insert(startIndex, event);
0240             endInsertRows();
0241         } else {
0242             if (*(newIt->incidence) != *(it->incidence)) {
0243                 const int startIndex = std::distance(std::begin(mEvents), it);
0244                 mEvents[startIndex] = event;
0245                 emit dataChanged(index(startIndex, 0), index(startIndex, 0), {});
0246             }
0247         }
0248     }
0249 
0250     if (!mInitialItemsLoaded && (!mSourceModel || mSourceModel->data({}, Sink::Store::ChildrenFetchedRole).toBool())) {
0251         mInitialItemsLoaded = true;
0252         emit initialItemsLoaded();
0253     }
0254 }
0255 
0256 
0257 bool EventOccurrenceModel::initialItemsComplete() const
0258 {
0259     return mInitialItemsLoaded;
0260 }
0261 
0262 QModelIndex EventOccurrenceModel::index(int row, int column, const QModelIndex &parent) const
0263 {
0264     if (!hasIndex(row, column, parent)) {
0265         return {};
0266     }
0267 
0268     if (!parent.isValid()) {
0269         return createIndex(row, column);
0270     }
0271     return {};
0272 }
0273 
0274 QModelIndex EventOccurrenceModel::parent(const QModelIndex &) const
0275 {
0276     return {};
0277 }
0278 
0279 int EventOccurrenceModel::rowCount(const QModelIndex &parent) const
0280 {
0281     if (!parent.isValid()) {
0282         return mEvents.size();
0283     }
0284     return 0;
0285 }
0286 
0287 int EventOccurrenceModel::columnCount(const QModelIndex &) const
0288 {
0289     return 1;
0290 }
0291 
0292 QByteArray EventOccurrenceModel::getColor(const QByteArray &calendar) const
0293 {
0294     const auto color = mCalendarCache->getProperty(calendar, "color").toByteArray();
0295     if (color.isEmpty()) {
0296         qWarning() << "Failed to get color for calendar " << calendar;
0297     }
0298     return color;
0299 }
0300 
0301 QVariant EventOccurrenceModel::data(const QModelIndex &idx, int role) const
0302 {
0303     if (!hasIndex(idx.row(), idx.column())) {
0304         return {};
0305     }
0306     auto event = mEvents.at(idx.row());
0307     auto icalEvent = event.incidence;
0308     switch (role) {
0309         case Summary:
0310             return icalEvent->summary();
0311         case Description:
0312             return icalEvent->description();
0313         case StartTime:
0314             return event.start;
0315         case EndTime:
0316             return event.end;
0317         case Color:
0318             return event.color;
0319         case AllDay:
0320             return event.allDay;
0321         case Event:
0322             return QVariant::fromValue(event.domainObject);
0323         case EventOccurrence:
0324             return QVariant::fromValue(event);
0325         default:
0326             SinkWarning() << "Unknown role for event:" << QMetaEnum::fromType<Roles>().valueToKey(role);
0327             return {};
0328     }
0329 }
0330